Barath commited on
Commit
4fd72e6
·
verified ·
1 Parent(s): 82c2e50

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +547 -486
app.py CHANGED
@@ -1,26 +1,191 @@
1
  #!/usr/bin/env python3
2
  """
3
- Headless Gradio MCP Server for VDI Heat Pump Data
4
- No UI - Pure MCP functionality for hosting
5
  """
6
 
7
- import gradio as gr
8
- import pandas as pd
9
- import numpy as np
10
  import os
11
  import sys
12
- import json
 
 
13
  import zipfile
14
  import tempfile
15
  from pathlib import Path
16
  import requests
17
  import xml.etree.ElementTree as ET
18
- from typing import Dict, List, Optional, Any
19
- import logging
20
 
21
- # Set up logging
22
- logging.basicConfig(level=logging.INFO)
23
- logger = logging.getLogger(__name__)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
  class VDIHeatPumpParser:
26
  """Main parser class for VDI heat pump data"""
@@ -30,170 +195,6 @@ class VDIHeatPumpParser:
30
  self.parsed_files = {}
31
  self.domain = "bim4hvac.com"
32
 
33
- def parse_010(self, r):
34
- """Parse 010:Vorlaufdaten"""
35
- return {
36
- 'blatt': r[1] if len(r) > 1 else None,
37
- 'hersteller': r[3] if len(r) > 3 else None,
38
- 'datum': r[4] if len(r) > 4 else None,
39
- }
40
-
41
- def parse_100(self, r):
42
- """Parse 100:Einsatzbereich"""
43
- return {
44
- 'index_100': int(r[1]) if len(r) > 1 else None,
45
- 'einsatzbereich': r[3] if len(r) > 3 else None,
46
- }
47
-
48
- def parse_110(self, r):
49
- """Parse 110:Typ"""
50
- return {
51
- 'index_110': int(r[1]) if len(r) > 1 else None,
52
- 'typ': r[3] if len(r) > 3 else None,
53
- }
54
-
55
- def parse_400(self, r):
56
- """Parse 400:Bauart"""
57
- return {
58
- 'index_400': int(r[1]) if len(r) > 1 else None,
59
- 'bauart': r[2] if len(r) > 2 else None,
60
- }
61
-
62
- def parse_450(self, r):
63
- """Parse 450:Aufstellungsort"""
64
- return {
65
- 'index_450': int(r[1]) if len(r) > 1 else None,
66
- 'aufstellungsort': r[2] if len(r) > 2 else None,
67
- }
68
-
69
- def parse_460(self, r):
70
- """Parse 460:Leistungsregelung der Wärmepumpe"""
71
- return {
72
- 'index_460': int(r[1]) if len(r) > 1 else None,
73
- 'leistungsregelung_der_wärmepumpe': r[2] if len(r) > 2 else None,
74
- }
75
-
76
- def parse_700(self, r):
77
- """Parse 700:Produktelementdaten"""
78
- return {
79
- 'index_700': int(r[1]) if len(r) > 1 else None,
80
- 'sortiernummer': r[2] if len(r) > 2 else None,
81
- 'produktname': r[3] if len(r) > 3 else None,
82
- 'heizleistung': r[4] if len(r) > 4 else None,
83
- 'leistungszahl': r[5] if len(r) > 5 else None,
84
- 'elektrische_aufnahmeleistung_wärmepumpe': r[6] if len(r) > 6 else None,
85
- 'leistung_der_elektrischen_zusatzheizung': r[7] if len(r) > 7 else None,
86
- 'elektroanschluss': r[8] if len(r) > 8 else None,
87
- 'anlaufstrom': r[9] if len(r) > 9 else None,
88
- 'index_auf_satzart_200_wärmequelle': r[10] if len(r) > 10 else None,
89
- 'eingesetztes_kältemittel': r[11] if len(r) > 11 else None,
90
- 'füllmenge_des_kältemittels': r[12] if len(r) > 12 else None,
91
- 'schallleistungspegel': r[13] if len(r) > 13 else None,
92
- 'schutzart_nach_din_en_60529': r[14] if len(r) > 14 else None,
93
- 'maximale_vorlauftemperatur': r[15] if len(r) > 15 else None,
94
- 'heizwassertemperaturspreizung': r[16] if len(r) > 16 else None,
95
- 'trinkwasser_erwärmung_über_indirekt_beheizten_speicher': r[17] if len(r) > 17 else None,
96
- 'kühlfunktion': r[19] if len(r) > 19 else None,
97
- 'kühlleistung': r[20] if len(r) > 20 else None,
98
- 'verdichteranzahl': r[21] if len(r) > 21 else None,
99
- 'leistungsstufen': r[22] if len(r) > 22 else None,
100
- 'produktserie': r[32] if len(r) > 32 else None,
101
- 'treibhauspotential_gwp': r[33] if len(r) > 33 else None,
102
- 'bauart_des_kältekreis': r[35] if len(r) > 35 else None,
103
- 'sicherheitsklasse_nach_din_en_378_1': r[36] if len(r) > 36 else None,
104
- 'hinweistext_zum_kältemittel': r[37] if len(r) > 37 else None,
105
- 'sanftanlasser': r[38] if len(r) > 38 else None,
106
- }
107
-
108
- def parse_710_01(self, r):
109
- """Parse 710.01:Heizungs-Wärmepumpe"""
110
- return {
111
- 'index_710_01': int(r[1]) if len(r) > 1 else None,
112
- 'korrekturfaktor_7_k': r[2] if len(r) > 2 else None,
113
- 'korrekturfaktor_10_k': r[3] if len(r) > 3 else None,
114
- 'temperaturdifferenz_am_verflüssiger_bei_prüfstandmessung': r[4] if len(r) > 4 else None,
115
- }
116
-
117
- def parse_710_04(self, r):
118
- """Parse 710.04:Wasser-Wasser-Wärmepumpe"""
119
- return {
120
- 'index_710_04': int(r[1]) if len(r) > 1 else None,
121
- 'leistungszahl_bei_w10_w35': r[2] if len(r) > 2 else None,
122
- 'elektrische_leistungsaufnahme_wärmequellenpumpe': r[3] if len(r) > 3 else None,
123
- 'heizleistung': r[4] if len(r) > 4 else None,
124
- 'einsatzgrenze_wärmequelle_von': r[5] if len(r) > 5 else None,
125
- 'einsatzgrenze_wärmequelle_bis': r[6] if len(r) > 6 else None,
126
- 'volumenstrom_heizungsseitig': r[7] if len(r) > 7 else None,
127
- 'wärmequellenpumpe_intern': r[8] if len(r) > 8 else None,
128
- 'heizkreispumpe_intern': r[9] if len(r) > 9 else None,
129
- 'elektrische_leistungsaufnahme_heizkreispumpe': r[10] if len(r) > 10 else None,
130
- 'leistungszahl_bei_w10_w45': r[11] if len(r) > 11 else None,
131
- 'leistungszahl_bei_w10_w55': r[12] if len(r) > 12 else None,
132
- 'leistungszahl_bei_w7_w35': r[13] if len(r) > 13 else None,
133
- 'kühlleistung_bei_w15_w23': r[14] if len(r) > 14 else None,
134
- 'kühlleistungszahl_bei_w15_w23': r[15] if len(r) > 15 else None,
135
- 'minimaler_volumenstrom_wärmequelle': r[16] if len(r) > 16 else None,
136
- 'maximaler_volumenstrom_wärmequelle': r[17] if len(r) > 17 else None,
137
- }
138
-
139
- def parse_710_05(self, r):
140
- """Parse 710.05:Luft-Wasser-Wärmepumpe"""
141
- return {
142
- 'index_710_05': int(r[1]) if len(r) > 1 else None,
143
- 'leistungszahl_bei_a_7_w35': r[2] if len(r) > 2 else None,
144
- 'leistungszahl_bei_a2_w35': r[3] if len(r) > 3 else None,
145
- 'leistungszahl_bei_a10_w35': r[4] if len(r) > 4 else None,
146
- 'abtauart': r[5] if len(r) > 5 else None,
147
- 'kühlfunktion_durch_kreislaufumkehr': r[6] if len(r) > 6 else None,
148
- 'leistungszahl_bei_a7_w35': r[7] if len(r) > 7 else None,
149
- 'leistungszahl_bei_a_15_w35': r[8] if len(r) > 8 else None,
150
- 'leistungszahl_bei_a2_w45': r[9] if len(r) > 9 else None,
151
- 'leistungszahl_bei_a7_w45': r[10] if len(r) > 10 else None,
152
- 'leistungszahl_bei_a_7_w55': r[11] if len(r) > 11 else None,
153
- 'leistungszahl_bei_a7_w55': r[12] if len(r) > 12 else None,
154
- 'leistungszahl_bei_a10_w55': r[13] if len(r) > 13 else None,
155
- 'kühlleistungszahl_bei_a35_w7': r[14] if len(r) > 14 else None,
156
- 'kühlleistungszahl_bei_a35_w18': r[15] if len(r) > 15 else None,
157
- 'kühlleistung_bei_a35_w7': r[16] if len(r) > 16 else None,
158
- 'kühlleistung_bei_a35_w18': r[17] if len(r) > 17 else None,
159
- 'minimale_einsatzgrenze_wärmequelle': r[18] if len(r) > 18 else None,
160
- 'maximale_einsatzgrenze_wärmequelle': r[19] if len(r) > 19 else None,
161
- 'leistungszahl_a20_w35': r[20] if len(r) > 20 else None,
162
- 'leistungszahl_a20_w45': r[21] if len(r) > 21 else None,
163
- 'leistungszahl_a20_w55': r[22] if len(r) > 22 else None,
164
- 'leistungsaufnahme_luefter': r[25] if len(r) > 25 else None,
165
- 'volumenstrom_heizungsseitig': r[26] if len(r) > 26 else None,
166
- }
167
-
168
- def parse_710_07(self, r):
169
- """Parse 710.07:Einbringmaße"""
170
- return {
171
- 'index_710_07': int(r[1]) if len(r) > 1 else None,
172
- 'art_der_maße': r[2] if len(r) > 2 else None,
173
- 'länge': r[3] if len(r) > 3 else None,
174
- 'breite': r[4] if len(r) > 4 else None,
175
- 'höhe': r[5] if len(r) > 5 else None,
176
- 'masse': r[6] if len(r) > 6 else None,
177
- 'beschreibung': r[7] if len(r) > 7 else None,
178
- }
179
-
180
- def parse_800(self, r):
181
- """Parse 800:TGA-Nummer"""
182
- return {
183
- 'index_800': int(r[1]) if len(r) > 1 else None,
184
- 'tga_nummer': r[2] if len(r) > 2 else None,
185
- }
186
-
187
- def parse_810(self, r):
188
- """Parse 810:Artikelnummern"""
189
- return {
190
- 'index_810': int(r[1]) if len(r) > 1 else None,
191
- 'artikelnummer': r[2] if len(r) > 2 else None,
192
- 'artikelname': r[9] if len(r) > 9 else None,
193
- 'energieeffizienzklasse': r[10] if len(r) > 10 else None,
194
- 'erp_richtlinie': r[11] if len(r) > 11 else None,
195
- }
196
-
197
  def check_catalogs_for_part(self, part: int) -> str:
198
  """Check if catalogs have been updated for a part"""
199
  try:
@@ -212,23 +213,26 @@ class VDIHeatPumpParser:
212
  return response.text
213
  except Exception as e:
214
  raise Exception(f"Error checking catalogs for part {part}: {str(e)}")
215
-
216
  def download_vdi_files(self, part: int, download_dir: str = None) -> List[str]:
217
  """Download all VDI files for a given part"""
218
- if download_dir is None:
219
- download_dir = os.path.join(tempfile.gettempdir(), f"vdi_part{part:02d}")
220
-
221
- os.makedirs(download_dir, exist_ok=True)
222
-
223
  try:
 
 
 
 
 
224
  # Get catalog information
225
  catalog_xml = self.check_catalogs_for_part(part)
 
 
226
  root = ET.fromstring(catalog_xml)
227
  result = root.find('.//{urn:bdhmcc}CheckCatalogsForPartResult')
228
 
229
  if result is None:
230
  raise Exception("No catalog results found")
231
 
 
232
  catalogs_xml = ET.fromstring(result.text)
233
 
234
  # Extract catalog information
@@ -241,6 +245,8 @@ class VDIHeatPumpParser:
241
  })
242
 
243
  df = pd.DataFrame(data)
 
 
244
  df['slug'] = df['nick'] + df['part']
245
  df['download_url'] = f"https://www.catalogue.{self.domain}/" + df['slug'] + "/Downloads/PART" + df['part'] + "_" + df['nick'] + ".zip"
246
 
@@ -250,92 +256,93 @@ class VDIHeatPumpParser:
250
  download_url = row['download_url']
251
  file_name = os.path.join(download_dir, f"PART{row['part']}_{row['nick']}.zip")
252
 
253
- try:
254
- response = requests.get(download_url, timeout=30)
255
- response.raise_for_status()
256
-
257
- with open(file_name, 'wb') as file:
258
- file.write(response.content)
259
-
260
- downloaded_files.append(file_name)
261
- logger.info(f"Downloaded: {file_name}")
262
- except Exception as e:
263
- logger.error(f"Failed to download {download_url}: {e}")
264
 
265
  return downloaded_files
266
 
267
  except Exception as e:
268
- logger.error(f"Error downloading VDI files: {e}")
269
- return []
270
-
271
  def parse_vdi_file(self, file_path: str, nick: str) -> pd.DataFrame:
272
- """Parse a single VDI file"""
273
  try:
274
  with open(file_path, 'r', encoding="latin-1") as file:
275
  text = file.read()
276
 
 
277
  lines = np.array(text.split("\n"))
278
  records = list(map(lambda x: x.split(";"), lines))
279
 
280
- # Initialize records
281
- r010 = self.parse_010([])
282
- r100 = self.parse_100([])
283
- r110 = self.parse_110([])
284
- r400 = self.parse_400([])
285
- r450 = self.parse_450([])
286
- r460 = self.parse_460([])
287
- r700 = self.parse_700([])
288
- r710_01 = self.parse_710_01([])
289
- r710_04 = self.parse_710_04([])
290
- r710_05 = self.parse_710_05([])
291
- r710_07 = self.parse_710_07([])
292
- r800 = self.parse_800([])
293
- r810 = self.parse_810([])
294
-
 
295
  data = []
296
  for r in records:
297
  if r[0] == '010':
298
- r010 = self.parse_010(r)
299
- elif r[0] == '100':
300
- r100 = self.parse_100(r)
301
  elif r[0] == '110':
302
- r110 = self.parse_110(r)
 
303
  r400s = []
304
  r450s = []
305
  r460s = []
306
  r700s = []
307
- r710_07s = {}
308
  elif r[0] == '400':
309
- r400 = self.parse_400(r)
310
  r400s.append(r400)
311
  elif r[0] == '450':
312
- r450 = self.parse_450(r)
313
  r450s.append(r450)
314
  elif r[0] == '460':
315
- r460 = self.parse_460(r)
316
  r460s.append(r460)
317
  elif r[0] == '700':
318
- r700 = self.parse_700(r)
319
  r700s.append(r700)
320
  elif r[0] == '710.01':
321
- r710_01 = self.parse_710_01(r)
322
  if r700s:
323
  r700s[-1].update(r710_01)
324
  elif r[0] == '710.04':
325
- r710_04 = self.parse_710_04(r)
326
  if r700s:
327
  r700s[-1].update(r710_04)
328
  elif r[0] == '710.05':
329
- r710_05 = self.parse_710_05(r)
330
  if r700s:
331
  r700s[-1].update(r710_05)
332
  elif r[0] == '710.07':
333
- r710_07 = self.parse_710_07(r)
334
  if r700s:
335
  r710_07s.setdefault(r700s[-1]['index_700'], []).append(r710_07)
336
  elif r[0] == '800':
337
- r800 = self.parse_800(r)
338
 
 
339
  if r800['tga_nummer'] and len(r800['tga_nummer']) >= 50:
340
  try:
341
  index_400 = int(r800['tga_nummer'][27:30])
@@ -343,11 +350,13 @@ class VDIHeatPumpParser:
343
  index_460 = int(r800['tga_nummer'][33:36])
344
  index_700 = int(r800['tga_nummer'][45:50])
345
 
 
346
  r400 = next((r for r in r400s if r['index_400'] == index_400), {})
347
  r450 = next((r for r in r450s if r['index_450'] == index_450), {})
348
  r460 = next((r for r in r460s if r['index_460'] == index_460), {})
349
  r700 = next((r for r in r700s if r['index_700'] == index_700), {})
350
 
 
351
  filtered_r710_07s = [r for r in r710_07s.get(index_700, []) if r['art_der_maße'] == '2']
352
  if len(filtered_r710_07s) == 1:
353
  r710_07 = filtered_r710_07s[0]
@@ -355,18 +364,27 @@ class VDIHeatPumpParser:
355
  aufstellungsort = r450.get('aufstellungsort', '').lower()
356
  r710_07 = next((r for r in filtered_r710_07s if r.get('beschreibung', '').lower().startswith(aufstellungsort)), {})
357
  except (ValueError, IndexError):
 
358
  r400, r450, r460, r700, r710_07 = {}, {}, {}, {}, {}
359
 
360
  elif r[0] == '810':
361
- r810 = self.parse_810(r)
362
  if r700:
363
  data.append({**r810, **r800, **r710_07, **r700, **r460, **r450, **r400, **r110, **r100, **r010})
364
 
 
365
  df = pd.DataFrame(data)
 
 
366
  df['nick'] = nick
 
 
367
  df = df.replace({r'[\r\n]+': ' '}, regex=True)
 
 
368
  df = df.replace({r'¶': ', '}, regex=True)
369
 
 
370
  index_columns = [col for col in df.columns if col.startswith('index_')]
371
  for col in index_columns:
372
  if col in df.columns:
@@ -375,9 +393,8 @@ class VDIHeatPumpParser:
375
  return df
376
 
377
  except Exception as e:
378
- logger.error(f"Error parsing VDI file {file_path}: {str(e)}")
379
- return pd.DataFrame()
380
-
381
  def extract_and_parse_zip(self, zip_path: str) -> pd.DataFrame:
382
  """Extract ZIP file and parse VDI files"""
383
  nick = Path(zip_path).stem.split('_')[-1]
@@ -386,13 +403,13 @@ class VDIHeatPumpParser:
386
  with zipfile.ZipFile(zip_path, 'r') as zip_ref:
387
  zip_ref.extractall(temp_dir)
388
 
 
389
  vdi_files = list(Path(temp_dir).rglob("*.vdi"))
390
  if not vdi_files:
391
  vdi_files = list(Path(temp_dir).rglob("*.VDI"))
392
 
393
  if not vdi_files:
394
- logger.warning(f"No VDI files found in {zip_path}")
395
- return pd.DataFrame()
396
 
397
  all_data = []
398
  for vdi_file in vdi_files:
@@ -401,7 +418,7 @@ class VDIHeatPumpParser:
401
  if not df.empty:
402
  all_data.append(df)
403
  except Exception as e:
404
- logger.error(f"Error parsing {vdi_file.name}: {e}")
405
  continue
406
 
407
  if all_data:
@@ -414,54 +431,14 @@ class VDIHeatPumpParser:
414
  # Initialize the parser
415
  parser = VDIHeatPumpParser()
416
 
417
- # MCP Tool Functions - These will be automatically exposed as MCP tools
418
- def download_vdi_files(part: str, download_dir: str = "") -> str:
419
- """
420
- Download VDI files for a specific part number.
421
-
422
- Args:
423
- part (str): Part number (e.g., 2 for heat generators, 22 for heat pumps)
424
- download_dir (str): Directory to save downloaded files (optional)
425
-
426
- Returns:
427
- str: JSON response with download results
428
- """
429
- try:
430
- part_int = int(part)
431
- download_dir = download_dir.strip() if download_dir.strip() else None
432
- downloaded_files = parser.download_vdi_files(part_int, download_dir)
433
-
434
- result = {
435
- "status": "success",
436
- "message": f"Successfully downloaded {len(downloaded_files)} VDI files for part {part_int}",
437
- "part": part_int,
438
- "files": downloaded_files,
439
- "download_directory": download_dir or f"temp directory for part {part_int}"
440
- }
441
- return json.dumps(result, indent=2)
442
- except Exception as e:
443
- return f"Error downloading VDI files: {str(e)}"
444
-
445
- def download_and_parse_part(part: str, download_dir: str = "") -> str:
446
- """
447
- Download and automatically parse all VDI files for a part.
448
-
449
- Args:
450
- part (str): Part number (e.g., 2 for heat generators, 22 for heat pumps)
451
- download_dir (str): Directory to save downloaded files (optional)
452
-
453
- Returns:
454
- str: JSON response with parsing results including heat pump count and manufacturers
455
- """
456
  try:
457
- part_int = int(part)
458
- download_dir = download_dir.strip() if download_dir.strip() else None
459
 
460
- # Download files first
461
- downloaded_files = parser.download_vdi_files(part_int, download_dir)
462
-
463
- if not downloaded_files:
464
- return f"No files were downloaded for part {part_int}. Check your internet connection or try a different part number."
465
 
466
  # Parse all downloaded files
467
  all_data = []
@@ -471,81 +448,36 @@ def download_and_parse_part(part: str, download_dir: str = "") -> str:
471
  if not df.empty:
472
  all_data.append(df)
473
  except Exception as e:
474
- logger.error(f"Error parsing {file_path}: {e}")
475
 
476
  if all_data:
477
  combined_df = pd.concat(all_data, ignore_index=True)
478
- cache_key = f"part_{part_int}"
479
  parser.parsed_files[cache_key] = combined_df
480
 
481
  result = {
482
  "status": "success",
483
- "message": f"Successfully downloaded and parsed {len(combined_df)} heat pump records for part {part_int}",
484
- "part": part_int,
485
  "record_count": len(combined_df),
486
- "manufacturers": sorted(combined_df['hersteller'].dropna().unique().tolist()) if 'hersteller' in combined_df.columns else [],
487
- "download_directory": download_dir or f"temp directory for part {part_int}"
488
  }
489
  else:
490
  result = {
491
  "status": "warning",
492
- "message": f"Files downloaded but no heat pump data found for part {part_int}",
493
- "part": part_int,
494
  "record_count": 0
495
  }
496
 
497
  return json.dumps(result, indent=2)
498
- except Exception as e:
499
- return f"Error downloading and parsing part {part}: {str(e)}"
500
-
501
- def parse_vdi_file(file_path: str) -> str:
502
- """
503
- Parse a VDI ZIP file and extract heat pump data.
504
-
505
- Args:
506
- file_path (str): Path to the VDI ZIP file
507
-
508
- Returns:
509
- str: JSON response with parsing results
510
- """
511
- try:
512
- if not os.path.exists(file_path):
513
- return f"File not found: {file_path}"
514
-
515
- df = parser.extract_and_parse_zip(file_path)
516
 
517
- result = {
518
- "status": "success",
519
- "message": f"Successfully parsed {len(df)} heat pump records",
520
- "file_path": file_path,
521
- "record_count": len(df)
522
- }
523
- return json.dumps(result, indent=2)
524
  except Exception as e:
525
- return f"Error parsing file: {str(e)}"
526
 
527
- def search_heatpump(
528
- manufacturer: str = "",
529
- product_name: str = "",
530
- article_number: str = "",
531
- heating_power_min: str = "",
532
- heating_power_max: str = "",
533
- heatpump_type: str = ""
534
- ) -> str:
535
- """
536
- Search for specific heat pump by criteria.
537
-
538
- Args:
539
- manufacturer (str): Manufacturer name (optional)
540
- product_name (str): Product name or partial match (optional)
541
- article_number (str): Article number (optional)
542
- heating_power_min (str): Minimum heating power in kW (optional)
543
- heating_power_max (str): Maximum heating power in kW (optional)
544
- heatpump_type (str): Heat pump type (e.g., 'Luft-Wasser') (optional)
545
-
546
- Returns:
547
- str: JSON array of matching heat pumps with their specifications
548
- """
549
  try:
550
  # Combine all parsed data
551
  all_data = []
@@ -553,100 +485,72 @@ def search_heatpump(
553
  all_data.append(df)
554
 
555
  if not all_data:
556
- return "No data available. Please download and parse VDI files first using download_and_parse_part."
557
 
558
  combined_df = pd.concat(all_data, ignore_index=True)
 
 
559
  filtered_df = combined_df.copy()
560
 
561
- # Apply filters with more flexible matching
562
- if manufacturer.strip():
563
- if 'hersteller' in filtered_df.columns:
564
- filtered_df = filtered_df[
565
- filtered_df['hersteller'].str.contains(manufacturer.strip(), case=False, na=False)
566
- ]
567
-
568
- if product_name.strip():
569
- # Search in both produktname and artikelname columns
570
- mask = pd.Series([False] * len(filtered_df))
571
- if 'produktname' in filtered_df.columns:
572
- mask |= filtered_df['produktname'].str.contains(product_name.strip(), case=False, na=False)
573
- if 'artikelname' in filtered_df.columns:
574
- mask |= filtered_df['artikelname'].str.contains(product_name.strip(), case=False, na=False)
575
- filtered_df = filtered_df[mask]
576
-
577
- if article_number.strip():
578
- if 'artikelnummer' in filtered_df.columns:
579
- filtered_df = filtered_df[
580
- filtered_df['artikelnummer'].str.contains(article_number.strip(), case=False, na=False)
581
- ]
582
-
583
- if heatpump_type.strip():
584
- if 'typ' in filtered_df.columns:
585
- filtered_df = filtered_df[
586
- filtered_df['typ'].str.contains(heatpump_type.strip(), case=False, na=False)
587
- ]
588
 
589
  # Filter by heating power
590
- if heating_power_min.strip() or heating_power_max.strip():
591
- if 'heizleistung' in filtered_df.columns:
592
- filtered_df['heizleistung_numeric'] = pd.to_numeric(filtered_df['heizleistung'], errors='coerce')
593
-
594
- if heating_power_min.strip():
595
- min_power = float(heating_power_min.strip())
596
- filtered_df = filtered_df[filtered_df['heizleistung_numeric'] >= min_power]
597
-
598
- if heating_power_max.strip():
599
- max_power = float(heating_power_max.strip())
600
- filtered_df = filtered_df[filtered_df['heizleistung_numeric'] <= max_power]
601
 
602
  # Return results
603
- result_columns = ['hersteller', 'produktname', 'artikelname', 'artikelnummer', 'heizleistung', 'typ', 'energieeffizienzklasse']
604
  available_columns = [col for col in result_columns if col in filtered_df.columns]
605
-
606
- if not available_columns:
607
- return "No matching columns found in the data."
608
-
609
  result_df = filtered_df[available_columns].head(20) # Limit to 20 results
610
 
611
- if result_df.empty:
612
- return "No heat pumps found matching your criteria."
613
-
614
  return result_df.to_json(orient='records', indent=2)
 
615
  except Exception as e:
616
- return f"Error searching heat pumps: {str(e)}"
617
 
618
  def get_heatpump_details(article_number: str) -> str:
619
- """
620
- Get detailed information about a specific heat pump.
621
-
622
- Args:
623
- article_number (str): Article number of the heat pump
624
-
625
- Returns:
626
- str: JSON object with all available heat pump specifications and technical details
627
- """
628
  try:
629
  # Search across all parsed data
630
  for df in parser.parsed_files.values():
631
- if 'artikelnummer' in df.columns:
632
- matching_rows = df[df['artikelnummer'] == article_number.strip()]
633
- if not matching_rows.empty:
634
- details = matching_rows.iloc[0].to_dict()
635
- # Clean up None values
636
- details = {k: v for k, v in details.items() if v is not None and v != ''}
637
- return json.dumps(details, indent=2, default=str)
638
-
639
- return f"Heat pump with article number '{article_number}' not found"
640
  except Exception as e:
641
- return f"Error getting heat pump details: {str(e)}"
642
 
643
  def list_manufacturers() -> str:
644
- """
645
- List all available manufacturers in parsed data.
646
-
647
- Returns:
648
- str: JSON object with list of manufacturers and count
649
- """
650
  try:
651
  all_manufacturers = set()
652
  for df in parser.parsed_files.values():
@@ -655,103 +559,260 @@ def list_manufacturers() -> str:
655
  all_manufacturers.update(manufacturers)
656
 
657
  if not all_manufacturers:
658
- return "No manufacturers available in the parsed data. Please parse VDI files first using download_and_parse_part."
 
 
 
 
 
659
 
660
  result = {
 
661
  "manufacturers": sorted(list(all_manufacturers)),
662
  "count": len(all_manufacturers)
663
  }
664
  return json.dumps(result, indent=2)
 
665
  except Exception as e:
666
- return f"Error listing manufacturers: {str(e)}"
667
 
668
- def check_storage() -> str:
669
- """
670
- Check available storage locations and permissions.
671
-
672
- Returns:
673
- str: JSON object with storage location information and recommendations
674
- """
675
  try:
676
- import tempfile
677
- storage_info = {}
 
 
 
 
678
 
679
- # Check various storage locations
680
- locations_to_check = [
681
- ("Current directory", "./data"),
682
- ("Home directory", os.path.expanduser("~/vdi_data")),
683
- ("Temp directory", os.path.join(tempfile.gettempdir(), "vdi_data")),
684
- ]
685
 
686
- for name, path in locations_to_check:
687
- try:
688
- os.makedirs(path, exist_ok=True)
689
- # Try to write a test file
690
- test_file = os.path.join(path, "test_write.txt")
691
- with open(test_file, 'w') as f:
692
- f.write("test")
693
- os.remove(test_file)
694
- storage_info[name] = {"path": path, "writable": True, "status": "OK"}
695
- except Exception as e:
696
- storage_info[name] = {"path": path, "writable": False, "status": str(e)}
697
 
698
  result = {
699
- "storage_locations": storage_info,
700
- "recommended": next((info["path"] for info in storage_info.values() if info["writable"]), tempfile.gettempdir())
 
 
 
701
  }
702
-
703
  return json.dumps(result, indent=2)
 
704
  except Exception as e:
705
- return f"Error checking storage: {str(e)}"
706
-
707
- # Auto-populate data on startup
708
- def initialize_data():
709
- """Initialize with some heat pump data automatically"""
710
- try:
711
- print("🚀 Initializing heat pump database...")
712
- result = download_and_parse_part("22") # Heat pumps
713
- print(f"✅ Initialization complete: {result}")
714
- except Exception as e:
715
- print(f"⚠️ Initialization failed: {e}")
716
-
717
- # Create a minimal Gradio interface with just the MCP functions
718
- # No actual UI components - just expose the functions
719
- def create_dummy_interface():
720
- """Create a minimal interface that exposes MCP functions without UI"""
721
- return gr.Interface(
722
- fn=lambda x: "MCP Server Running - No UI",
723
- inputs=gr.Textbox(visible=False),
724
- outputs=gr.Textbox(visible=False),
725
- title="VDI Heat Pump MCP Server",
726
- description="Headless MCP server for heat pump data"
727
- )
728
 
729
- # Create the minimal interface
730
- demo = create_dummy_interface()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
731
 
732
- # Launch the headless MCP server
733
  if __name__ == "__main__":
734
- print("=" * 60)
735
- print("🔥 VDI Heat Pump Parser - Headless MCP Server")
736
- print("=" * 60)
737
- print("🔌 Starting headless Gradio MCP server...")
738
- print("📡 MCP tools will be automatically exposed for Claude Desktop")
739
- print("🚫 No web UI - pure MCP functionality")
740
- print("=" * 60)
741
 
742
- # Initialize data in background
743
- import threading
744
- init_thread = threading.Thread(target=initialize_data)
745
- init_thread.daemon = True
746
- init_thread.start()
747
-
748
- # Launch with MCP server enabled but no UI
749
  demo.launch(
750
- mcp_server=True, # Enable MCP server
751
- share=False, # No sharing
752
- server_name="0.0.0.0", # Listen on all interfaces
753
- server_port=7860, # Standard port
754
- show_error=True, # Show errors
755
- inbrowser=False, # Don't open browser
756
- quiet=True # Minimal logging
757
- )
 
1
  #!/usr/bin/env python3
2
  """
3
+ Gradio MCP Server for VDI Heat Pump Data Parsing and Querying
4
+ Provides tools to download, parse VDI files and query specific heat pump information
5
  """
6
 
7
+ import asyncio
8
+ import json
 
9
  import os
10
  import sys
11
+ import pandas as pd
12
+ import numpy as np
13
+ from typing import Any, Dict, List, Optional
14
  import zipfile
15
  import tempfile
16
  from pathlib import Path
17
  import requests
18
  import xml.etree.ElementTree as ET
19
+ import gradio as gr
 
20
 
21
+ # Add error logging
22
+ import logging
23
+ logging.basicConfig(level=logging.DEBUG)
24
+
25
+ # Import your existing parsing functions
26
+ def parse_010(r):
27
+ """Parse 010:Vorlaufdaten"""
28
+ return {
29
+ 'blatt': r[1] if len(r) > 1 else None,
30
+ 'hersteller': r[3] if len(r) > 3 else None,
31
+ 'datum': r[4] if len(r) > 4 else None,
32
+ }
33
+
34
+ def parse_100(r):
35
+ """Parse 100:Einsatzbereich"""
36
+ return {
37
+ 'index_100': int(r[1]) if len(r) > 1 else None,
38
+ 'einsatzbereich': r[3] if len(r) > 3 else None,
39
+ }
40
+
41
+ def parse_110(r):
42
+ """Parse 110:Typ"""
43
+ return {
44
+ 'index_110': int(r[1]) if len(r) > 1 else None,
45
+ 'typ': r[3] if len(r) > 3 else None,
46
+ }
47
+
48
+ def parse_400(r):
49
+ """Parse 400:Bauart"""
50
+ return {
51
+ 'index_400': int(r[1]) if len(r) > 1 else None,
52
+ 'bauart': r[2] if len(r) > 2 else None,
53
+ }
54
+
55
+ def parse_450(r):
56
+ """Parse 450:Aufstellungsort"""
57
+ return {
58
+ 'index_450': int(r[1]) if len(r) > 1 else None,
59
+ 'aufstellungsort': r[2] if len(r) > 2 else None,
60
+ }
61
+
62
+ def parse_460(r):
63
+ """Parse 460:Leistungsregelung der Wärmepumpe"""
64
+ return {
65
+ 'index_460': int(r[1]) if len(r) > 1 else None,
66
+ 'leistungsregelung_der_wärmepumpe': r[2] if len(r) > 2 else None,
67
+ }
68
+
69
+ def parse_700(r):
70
+ """Parse 700:Produktelementdaten"""
71
+ return {
72
+ 'index_700': int(r[1]) if len(r) > 1 else None,
73
+ 'sortiernummer': r[2] if len(r) > 2 else None,
74
+ 'produktname': r[3] if len(r) > 3 else None,
75
+ 'heizleistung': r[4] if len(r) > 4 else None,
76
+ 'leistungszahl': r[5] if len(r) > 5 else None,
77
+ 'elektrische_aufnahmeleistung_wärmepumpe': r[6] if len(r) > 6 else None,
78
+ 'leistung_der_elektrischen_zusatzheizung': r[7] if len(r) > 7 else None,
79
+ 'elektroanschluss': r[8] if len(r) > 8 else None,
80
+ 'anlaufstrom': r[9] if len(r) > 9 else None,
81
+ 'index_auf_satzart_200_wärmequelle': r[10] if len(r) > 10 else None,
82
+ 'eingesetztes_kältemittel': r[11] if len(r) > 11 else None,
83
+ 'füllmenge_des_kältemittels': r[12] if len(r) > 12 else None,
84
+ 'schallleistungspegel': r[13] if len(r) > 13 else None,
85
+ 'schutzart_nach_din_en_60529': r[14] if len(r) > 14 else None,
86
+ 'maximale_vorlauftemperatur': r[15] if len(r) > 15 else None,
87
+ 'heizwassertemperaturspreizung': r[16] if len(r) > 16 else None,
88
+ 'trinkwasser_erwärmung_über_indirekt_beheizten_speicher': r[17] if len(r) > 17 else None,
89
+ 'kühlfunktion': r[19] if len(r) > 19 else None,
90
+ 'kühlleistung': r[20] if len(r) > 20 else None,
91
+ 'verdichteranzahl': r[21] if len(r) > 21 else None,
92
+ 'leistungsstufen': r[22] if len(r) > 22 else None,
93
+ 'produktserie': r[32] if len(r) > 32 else None,
94
+ 'treibhauspotential_gwp': r[33] if len(r) > 33 else None,
95
+ 'bauart_des_kältekreis': r[35] if len(r) > 35 else None,
96
+ 'sicherheitsklasse_nach_din_en_378_1': r[36] if len(r) > 36 else None,
97
+ 'hinweistext_zum_kältemittel': r[37] if len(r) > 37 else None,
98
+ 'sanftanlasser': r[38] if len(r) > 38 else None,
99
+ }
100
+
101
+ def parse_710_01(r):
102
+ """Parse 710.01:Heizungs-Wärmepumpe"""
103
+ return {
104
+ 'index_710_01': int(r[1]) if len(r) > 1 else None,
105
+ 'korrekturfaktor_7_k': r[2] if len(r) > 2 else None,
106
+ 'korrekturfaktor_10_k': r[3] if len(r) > 3 else None,
107
+ 'temperaturdifferenz_am_verflüssiger_bei_prüfstandmessung': r[4] if len(r) > 4 else None,
108
+ }
109
+
110
+ def parse_710_04(r):
111
+ """Parse 710.04:Wasser-Wasser-Wärmepumpe"""
112
+ return {
113
+ 'index_710_04': int(r[1]) if len(r) > 1 else None,
114
+ 'leistungszahl_bei_w10_w35': r[2] if len(r) > 2 else None,
115
+ 'elektrische_leistungsaufnahme_wärmequellenpumpe': r[3] if len(r) > 3 else None,
116
+ 'heizleistung': r[4] if len(r) > 4 else None,
117
+ 'einsatzgrenze_wärmequelle_von': r[5] if len(r) > 5 else None,
118
+ 'einsatzgrenze_wärmequelle_bis': r[6] if len(r) > 6 else None,
119
+ 'volumenstrom_heizungsseitig': r[7] if len(r) > 7 else None,
120
+ 'wärmequellenpumpe_intern': r[8] if len(r) > 8 else None,
121
+ 'heizkreispumpe_intern': r[9] if len(r) > 9 else None,
122
+ 'elektrische_leistungsaufnahme_heizkreispumpe': r[10] if len(r) > 10 else None,
123
+ 'leistungszahl_bei_w10_w45': r[11] if len(r) > 11 else None,
124
+ 'leistungszahl_bei_w10_w55': r[12] if len(r) > 12 else None,
125
+ 'leistungszahl_bei_w7_w35': r[13] if len(r) > 13 else None,
126
+ 'kühlleistung_bei_w15_w23': r[14] if len(r) > 14 else None,
127
+ 'kühlleistungszahl_bei_w15_w23': r[15] if len(r) > 15 else None,
128
+ 'minimaler_volumenstrom_wärmequelle': r[16] if len(r) > 16 else None,
129
+ 'maximaler_volumenstrom_wärmequelle': r[17] if len(r) > 17 else None,
130
+ }
131
+
132
+ def parse_710_05(r):
133
+ """Parse 710.05:Luft-Wasser-Wärmepumpe"""
134
+ return {
135
+ 'index_710_05': int(r[1]) if len(r) > 1 else None,
136
+ 'leistungszahl_bei_a_7_w35': r[2] if len(r) > 2 else None,
137
+ 'leistungszahl_bei_a2_w35': r[3] if len(r) > 3 else None,
138
+ 'leistungszahl_bei_a10_w35': r[4] if len(r) > 4 else None,
139
+ 'abtauart': r[5] if len(r) > 5 else None,
140
+ 'kühlfunktion_durch_kreislaufumkehr': r[6] if len(r) > 6 else None,
141
+ 'leistungszahl_bei_a7_w35': r[7] if len(r) > 7 else None,
142
+ 'leistungszahl_bei_a_15_w35': r[8] if len(r) > 8 else None,
143
+ 'leistungszahl_bei_a2_w45': r[9] if len(r) > 9 else None,
144
+ 'leistungszahl_bei_a7_w45': r[10] if len(r) > 10 else None,
145
+ 'leistungszahl_bei_a_7_w55': r[11] if len(r) > 11 else None,
146
+ 'leistungszahl_bei_a7_w55': r[12] if len(r) > 12 else None,
147
+ 'leistungszahl_bei_a10_w55': r[13] if len(r) > 13 else None,
148
+ 'kühlleistungszahl_bei_a35_w7': r[14] if len(r) > 14 else None,
149
+ 'kühlleistungszahl_bei_a35_w18': r[15] if len(r) > 15 else None,
150
+ 'kühlleistung_bei_a35_w7': r[16] if len(r) > 16 else None,
151
+ 'kühlleistung_bei_a35_w18': r[17] if len(r) > 17 else None,
152
+ 'minimale_einsatzgrenze_wärmequelle': r[18] if len(r) > 18 else None,
153
+ 'maximale_einsatzgrenze_wärmequelle': r[19] if len(r) > 19 else None,
154
+ 'leistungszahl_a20_w35': r[20] if len(r) > 20 else None,
155
+ 'leistungszahl_a20_w45': r[21] if len(r) > 21 else None,
156
+ 'leistungszahl_a20_w55': r[22] if len(r) > 22 else None,
157
+ 'leistungsaufnahme_luefter': r[25] if len(r) > 25 else None,
158
+ 'volumenstrom_heizungsseitig': r[26] if len(r) > 26 else None,
159
+ }
160
+
161
+ def parse_710_07(r):
162
+ """Parse 710.07:Einbringmaße"""
163
+ return {
164
+ 'index_710_07': int(r[1]) if len(r) > 1 else None,
165
+ 'art_der_maße': r[2] if len(r) > 2 else None,
166
+ 'länge': r[3] if len(r) > 3 else None,
167
+ 'breite': r[4] if len(r) > 4 else None,
168
+ 'höhe': r[5] if len(r) > 5 else None,
169
+ 'masse': r[6] if len(r) > 6 else None,
170
+ 'beschreibung': r[7] if len(r) > 7 else None,
171
+ }
172
+
173
+ def parse_800(r):
174
+ """Parse 800:TGA-Nummer"""
175
+ return {
176
+ 'index_800': int(r[1]) if len(r) > 1 else None,
177
+ 'tga_nummer': r[2] if len(r) > 2 else None,
178
+ }
179
+
180
+ def parse_810(r):
181
+ """Parse 810:Artikelnummern"""
182
+ return {
183
+ 'index_810': int(r[1]) if len(r) > 1 else None,
184
+ 'artikelnummer': r[2] if len(r) > 2 else None,
185
+ 'artikelname': r[9] if len(r) > 9 else None,
186
+ 'energieeffizienzklasse': r[10] if len(r) > 10 else None,
187
+ 'erp_richtlinie': r[11] if len(r) > 11 else None,
188
+ }
189
 
190
  class VDIHeatPumpParser:
191
  """Main parser class for VDI heat pump data"""
 
195
  self.parsed_files = {}
196
  self.domain = "bim4hvac.com"
197
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  def check_catalogs_for_part(self, part: int) -> str:
199
  """Check if catalogs have been updated for a part"""
200
  try:
 
213
  return response.text
214
  except Exception as e:
215
  raise Exception(f"Error checking catalogs for part {part}: {str(e)}")
216
+
217
  def download_vdi_files(self, part: int, download_dir: str = None) -> List[str]:
218
  """Download all VDI files for a given part"""
 
 
 
 
 
219
  try:
220
+ if download_dir is None:
221
+ download_dir = os.path.join(tempfile.gettempdir(), f"vdi_part{part:02d}")
222
+
223
+ os.makedirs(download_dir, exist_ok=True)
224
+
225
  # Get catalog information
226
  catalog_xml = self.check_catalogs_for_part(part)
227
+
228
+ # Parse the XML response
229
  root = ET.fromstring(catalog_xml)
230
  result = root.find('.//{urn:bdhmcc}CheckCatalogsForPartResult')
231
 
232
  if result is None:
233
  raise Exception("No catalog results found")
234
 
235
+ # Parse the catalog data
236
  catalogs_xml = ET.fromstring(result.text)
237
 
238
  # Extract catalog information
 
245
  })
246
 
247
  df = pd.DataFrame(data)
248
+
249
+ # Build download URLs
250
  df['slug'] = df['nick'] + df['part']
251
  df['download_url'] = f"https://www.catalogue.{self.domain}/" + df['slug'] + "/Downloads/PART" + df['part'] + "_" + df['nick'] + ".zip"
252
 
 
256
  download_url = row['download_url']
257
  file_name = os.path.join(download_dir, f"PART{row['part']}_{row['nick']}.zip")
258
 
259
+ print(f"Downloading {download_url}...")
260
+ response = requests.get(download_url, timeout=30)
261
+ response.raise_for_status()
262
+
263
+ with open(file_name, 'wb') as file:
264
+ file.write(response.content)
265
+
266
+ downloaded_files.append(file_name)
267
+ print(f"Downloaded: {file_name}")
 
 
268
 
269
  return downloaded_files
270
 
271
  except Exception as e:
272
+ raise Exception(f"Error downloading VDI files for part {part}: {str(e)}")
273
+
 
274
  def parse_vdi_file(self, file_path: str, nick: str) -> pd.DataFrame:
275
+ """Parse a single VDI file using the full parsing logic"""
276
  try:
277
  with open(file_path, 'r', encoding="latin-1") as file:
278
  text = file.read()
279
 
280
+ # Read VDI file into lines
281
  lines = np.array(text.split("\n"))
282
  records = list(map(lambda x: x.split(";"), lines))
283
 
284
+ # Initialize hierarchical records
285
+ r010 = parse_010([])
286
+ r100 = parse_100([])
287
+ r110 = parse_110([])
288
+ r400 = parse_400([])
289
+ r450 = parse_450([])
290
+ r460 = parse_460([])
291
+ r700 = parse_700([])
292
+ r710_01 = parse_710_01([])
293
+ r710_04 = parse_710_04([])
294
+ r710_05 = parse_710_05([])
295
+ r710_07 = parse_710_07([])
296
+ r800 = parse_800([])
297
+ r810 = parse_810([])
298
+
299
+ # Parse records
300
  data = []
301
  for r in records:
302
  if r[0] == '010':
303
+ r010 = parse_010(r)
304
+ if r[0] == '100':
305
+ r100 = parse_100(r)
306
  elif r[0] == '110':
307
+ r110 = parse_110(r)
308
+ # keep track of 400s and 700s for merging with 800s
309
  r400s = []
310
  r450s = []
311
  r460s = []
312
  r700s = []
313
+ r710_07s = {} # capture list of 710.07 records by index_700
314
  elif r[0] == '400':
315
+ r400 = parse_400(r)
316
  r400s.append(r400)
317
  elif r[0] == '450':
318
+ r450 = parse_450(r)
319
  r450s.append(r450)
320
  elif r[0] == '460':
321
+ r460 = parse_460(r)
322
  r460s.append(r460)
323
  elif r[0] == '700':
324
+ r700 = parse_700(r)
325
  r700s.append(r700)
326
  elif r[0] == '710.01':
327
+ r710_01 = parse_710_01(r)
328
  if r700s:
329
  r700s[-1].update(r710_01)
330
  elif r[0] == '710.04':
331
+ r710_04 = parse_710_04(r)
332
  if r700s:
333
  r700s[-1].update(r710_04)
334
  elif r[0] == '710.05':
335
+ r710_05 = parse_710_05(r)
336
  if r700s:
337
  r700s[-1].update(r710_05)
338
  elif r[0] == '710.07':
339
+ r710_07 = parse_710_07(r)
340
  if r700s:
341
  r710_07s.setdefault(r700s[-1]['index_700'], []).append(r710_07)
342
  elif r[0] == '800':
343
+ r800 = parse_800(r)
344
 
345
+ # deconstruct indexes from TGA number
346
  if r800['tga_nummer'] and len(r800['tga_nummer']) >= 50:
347
  try:
348
  index_400 = int(r800['tga_nummer'][27:30])
 
350
  index_460 = int(r800['tga_nummer'][33:36])
351
  index_700 = int(r800['tga_nummer'][45:50])
352
 
353
+ # get corresponding records
354
  r400 = next((r for r in r400s if r['index_400'] == index_400), {})
355
  r450 = next((r for r in r450s if r['index_450'] == index_450), {})
356
  r460 = next((r for r in r460s if r['index_460'] == index_460), {})
357
  r700 = next((r for r in r700s if r['index_700'] == index_700), {})
358
 
359
+ # pick measurement record 710.07 (2=Aufstellmaße)
360
  filtered_r710_07s = [r for r in r710_07s.get(index_700, []) if r['art_der_maße'] == '2']
361
  if len(filtered_r710_07s) == 1:
362
  r710_07 = filtered_r710_07s[0]
 
364
  aufstellungsort = r450.get('aufstellungsort', '').lower()
365
  r710_07 = next((r for r in filtered_r710_07s if r.get('beschreibung', '').lower().startswith(aufstellungsort)), {})
366
  except (ValueError, IndexError):
367
+ # Handle malformed TGA numbers
368
  r400, r450, r460, r700, r710_07 = {}, {}, {}, {}, {}
369
 
370
  elif r[0] == '810':
371
+ r810 = parse_810(r)
372
  if r700:
373
  data.append({**r810, **r800, **r710_07, **r700, **r460, **r450, **r400, **r110, **r100, **r010})
374
 
375
+ # Create DataFrame
376
  df = pd.DataFrame(data)
377
+
378
+ # Add nick
379
  df['nick'] = nick
380
+
381
+ # Remove newlines in all cells
382
  df = df.replace({r'[\r\n]+': ' '}, regex=True)
383
+
384
+ # Replace ¶ with comma
385
  df = df.replace({r'¶': ', '}, regex=True)
386
 
387
+ # Cast all columns starting with 'index_' as integer
388
  index_columns = [col for col in df.columns if col.startswith('index_')]
389
  for col in index_columns:
390
  if col in df.columns:
 
393
  return df
394
 
395
  except Exception as e:
396
+ raise Exception(f"Error parsing VDI file {file_path}: {str(e)}")
397
+
 
398
  def extract_and_parse_zip(self, zip_path: str) -> pd.DataFrame:
399
  """Extract ZIP file and parse VDI files"""
400
  nick = Path(zip_path).stem.split('_')[-1]
 
403
  with zipfile.ZipFile(zip_path, 'r') as zip_ref:
404
  zip_ref.extractall(temp_dir)
405
 
406
+ # Find VDI files recursively
407
  vdi_files = list(Path(temp_dir).rglob("*.vdi"))
408
  if not vdi_files:
409
  vdi_files = list(Path(temp_dir).rglob("*.VDI"))
410
 
411
  if not vdi_files:
412
+ raise Exception(f"No VDI files found in {zip_path}")
 
413
 
414
  all_data = []
415
  for vdi_file in vdi_files:
 
418
  if not df.empty:
419
  all_data.append(df)
420
  except Exception as e:
421
+ print(f"Error parsing {vdi_file.name}: {e}")
422
  continue
423
 
424
  if all_data:
 
431
  # Initialize the parser
432
  parser = VDIHeatPumpParser()
433
 
434
+ # MCP Tool Functions
435
+ def download_and_parse_part(part: int) -> str:
436
+ """Download and parse VDI files for a specific part"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
437
  try:
438
+ download_dir = os.path.join(tempfile.gettempdir(), f"vdi_part{part:02d}")
 
439
 
440
+ # Download files
441
+ downloaded_files = parser.download_vdi_files(part, download_dir)
 
 
 
442
 
443
  # Parse all downloaded files
444
  all_data = []
 
448
  if not df.empty:
449
  all_data.append(df)
450
  except Exception as e:
451
+ print(f"Error parsing {file_path}: {e}")
452
 
453
  if all_data:
454
  combined_df = pd.concat(all_data, ignore_index=True)
455
+ cache_key = f"part_{part}"
456
  parser.parsed_files[cache_key] = combined_df
457
 
458
  result = {
459
  "status": "success",
460
+ "message": f"Successfully downloaded and parsed {len(combined_df)} heat pump records for part {part}",
461
+ "part": part,
462
  "record_count": len(combined_df),
463
+ "manufacturers": sorted(combined_df['hersteller'].dropna().unique().tolist())
 
464
  }
465
  else:
466
  result = {
467
  "status": "warning",
468
+ "message": f"No heat pump data found for part {part}",
469
+ "part": part,
470
  "record_count": 0
471
  }
472
 
473
  return json.dumps(result, indent=2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
474
 
 
 
 
 
 
 
 
475
  except Exception as e:
476
+ return json.dumps({"status": "error", "message": f"Error: {str(e)}"}, indent=2)
477
 
478
+ def search_heatpump(manufacturer: str = None, product_name: str = None, article_number: str = None,
479
+ heating_power_min: float = None, heating_power_max: float = None, heat_pump_type: str = None) -> str:
480
+ """Search for heat pumps based on criteria"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
481
  try:
482
  # Combine all parsed data
483
  all_data = []
 
485
  all_data.append(df)
486
 
487
  if not all_data:
488
+ return json.dumps({"status": "error", "message": "No data available. Please parse VDI files first."})
489
 
490
  combined_df = pd.concat(all_data, ignore_index=True)
491
+
492
+ # Apply filters
493
  filtered_df = combined_df.copy()
494
 
495
+ if manufacturer:
496
+ filtered_df = filtered_df[
497
+ filtered_df['hersteller'].str.contains(manufacturer, case=False, na=False)
498
+ ]
499
+
500
+ if product_name:
501
+ filtered_df = filtered_df[
502
+ filtered_df['produktname'].str.contains(product_name, case=False, na=False)
503
+ ]
504
+
505
+ if article_number:
506
+ filtered_df = filtered_df[
507
+ filtered_df['artikelnummer'].str.contains(article_number, case=False, na=False)
508
+ ]
509
+
510
+ if heat_pump_type:
511
+ filtered_df = filtered_df[
512
+ filtered_df['typ'].str.contains(heat_pump_type, case=False, na=False)
513
+ ]
 
 
 
 
 
 
 
 
514
 
515
  # Filter by heating power
516
+ if heating_power_min is not None or heating_power_max is not None:
517
+ filtered_df['heizleistung_numeric'] = pd.to_numeric(filtered_df['heizleistung'], errors='coerce')
518
+
519
+ if heating_power_min is not None:
520
+ filtered_df = filtered_df[filtered_df['heizleistung_numeric'] >= heating_power_min]
521
+
522
+ if heating_power_max is not None:
523
+ filtered_df = filtered_df[filtered_df['heizleistung_numeric'] <= heating_power_max]
 
 
 
524
 
525
  # Return results
526
+ result_columns = ['hersteller', 'produktname', 'artikelnummer', 'heizleistung', 'typ', 'energieeffizienzklasse']
527
  available_columns = [col for col in result_columns if col in filtered_df.columns]
 
 
 
 
528
  result_df = filtered_df[available_columns].head(20) # Limit to 20 results
529
 
 
 
 
530
  return result_df.to_json(orient='records', indent=2)
531
+
532
  except Exception as e:
533
+ return json.dumps({"status": "error", "message": f"Error searching heat pumps: {str(e)}"}, indent=2)
534
 
535
  def get_heatpump_details(article_number: str) -> str:
536
+ """Get detailed information about a specific heat pump"""
 
 
 
 
 
 
 
 
537
  try:
538
  # Search across all parsed data
539
  for df in parser.parsed_files.values():
540
+ matching_rows = df[df['artikelnummer'] == article_number]
541
+ if not matching_rows.empty:
542
+ details = matching_rows.iloc[0].to_dict()
543
+ # Clean up None values
544
+ details = {k: v for k, v in details.items() if v is not None and v != ''}
545
+ return json.dumps(details, indent=2, default=str)
546
+
547
+ return json.dumps({"status": "error", "message": f"Heat pump with article number '{article_number}' not found"})
548
+
549
  except Exception as e:
550
+ return json.dumps({"status": "error", "message": f"Error getting heat pump details: {str(e)}"}, indent=2)
551
 
552
  def list_manufacturers() -> str:
553
+ """List all manufacturers in the parsed data"""
 
 
 
 
 
554
  try:
555
  all_manufacturers = set()
556
  for df in parser.parsed_files.values():
 
559
  all_manufacturers.update(manufacturers)
560
 
561
  if not all_manufacturers:
562
+ return json.dumps({
563
+ "status": "warning",
564
+ "message": "No manufacturers available. Please parse VDI files first.",
565
+ "manufacturers": [],
566
+ "count": 0
567
+ })
568
 
569
  result = {
570
+ "status": "success",
571
  "manufacturers": sorted(list(all_manufacturers)),
572
  "count": len(all_manufacturers)
573
  }
574
  return json.dumps(result, indent=2)
575
+
576
  except Exception as e:
577
+ return json.dumps({"status": "error", "message": f"Error listing manufacturers: {str(e)}"}, indent=2)
578
 
579
+ def get_data_summary() -> str:
580
+ """Get summary statistics of all parsed data"""
 
 
 
 
 
581
  try:
582
+ if not parser.parsed_files:
583
+ return json.dumps({
584
+ "status": "warning",
585
+ "message": "No data available. Please parse VDI files first.",
586
+ "total_records": 0
587
+ })
588
 
589
+ total_records = 0
590
+ manufacturers = set()
591
+ heat_pump_types = set()
 
 
 
592
 
593
+ for df in parser.parsed_files.values():
594
+ total_records += len(df)
595
+ if 'hersteller' in df.columns:
596
+ manufacturers.update(df['hersteller'].dropna().unique())
597
+ if 'typ' in df.columns:
598
+ heat_pump_types.update(df['typ'].dropna().unique())
 
 
 
 
 
599
 
600
  result = {
601
+ "status": "success",
602
+ "total_records": total_records,
603
+ "manufacturer_count": len(manufacturers),
604
+ "heat_pump_types": sorted(list(heat_pump_types)),
605
+ "parsed_files": len(parser.parsed_files)
606
  }
 
607
  return json.dumps(result, indent=2)
608
+
609
  except Exception as e:
610
+ return json.dumps({"status": "error", "message": f"Error getting data summary: {str(e)}"}, indent=2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
611
 
612
+ # Create Gradio interface following the official MCP pattern
613
+ def create_interface():
614
+ """Create Gradio interface with MCP server"""
615
+
616
+ def test_download_and_parse(part_number):
617
+ """Test the download and parse functionality"""
618
+ if not part_number:
619
+ return "Please enter a part number"
620
+
621
+ try:
622
+ part = int(part_number)
623
+ result = download_and_parse_part(part)
624
+ return result
625
+ except ValueError:
626
+ return "Please enter a valid integer for part number"
627
+ except Exception as e:
628
+ return f"Error: {str(e)}"
629
+
630
+ def test_search(manufacturer, product_name, article_number, heating_power_min, heating_power_max, heat_pump_type):
631
+ """Test the search functionality"""
632
+ try:
633
+ # Convert empty strings to None
634
+ manufacturer = manufacturer if manufacturer.strip() else None
635
+ product_name = product_name if product_name.strip() else None
636
+ article_number = article_number if article_number.strip() else None
637
+ heat_pump_type = heat_pump_type if heat_pump_type.strip() else None
638
+
639
+ # Convert power values
640
+ heating_power_min = float(heating_power_min) if heating_power_min else None
641
+ heating_power_max = float(heating_power_max) if heating_power_max else None
642
+
643
+ result = search_heatpump(
644
+ manufacturer=manufacturer,
645
+ product_name=product_name,
646
+ article_number=article_number,
647
+ heating_power_min=heating_power_min,
648
+ heating_power_max=heating_power_max,
649
+ heat_pump_type=heat_pump_type
650
+ )
651
+ return result
652
+ except Exception as e:
653
+ return f"Error: {str(e)}"
654
+
655
+ def test_get_details(article_number):
656
+ """Test getting heat pump details"""
657
+ if not article_number.strip():
658
+ return "Please enter an article number"
659
+
660
+ result = get_heatpump_details(article_number.strip())
661
+ return result
662
+
663
+ def test_list_manufacturers():
664
+ """Test listing manufacturers"""
665
+ result = list_manufacturers()
666
+ return result
667
+
668
+ def test_get_summary():
669
+ """Test getting data summary"""
670
+ result = get_data_summary()
671
+ return result
672
+
673
+ # Create Gradio interface
674
+ with gr.Blocks(title="VDI Heat Pump Parser - MCP Server") as demo:
675
+ gr.Markdown("# VDI Heat Pump Parser - MCP Server")
676
+ gr.Markdown("This interface allows you to test the MCP tools for parsing VDI heat pump data.")
677
+
678
+ with gr.Tab("Download & Parse"):
679
+ with gr.Row():
680
+ part_input = gr.Textbox(label="Part Number", placeholder="Enter part number (e.g., 22 for heat pumps)")
681
+ download_btn = gr.Button("Download & Parse")
682
+ download_output = gr.Textbox(label="Result", lines=10)
683
+ download_btn.click(test_download_and_parse, inputs=[part_input], outputs=[download_output])
684
+
685
+ with gr.Tab("Search Heat Pumps"):
686
+ with gr.Row():
687
+ with gr.Column():
688
+ search_manufacturer = gr.Textbox(label="Manufacturer", placeholder="e.g., Viessmann")
689
+ search_product = gr.Textbox(label="Product Name", placeholder="e.g., Vitocal")
690
+ search_article = gr.Textbox(label="Article Number", placeholder="e.g., 12345")
691
+ with gr.Column():
692
+ search_power_min = gr.Textbox(label="Min Heating Power (kW)", placeholder="e.g., 5")
693
+ search_power_max = gr.Textbox(label="Max Heating Power (kW)", placeholder="e.g., 15")
694
+ search_type = gr.Textbox(label="Heat Pump Type", placeholder="e.g., Luft-Wasser")
695
+
696
+ search_btn = gr.Button("Search")
697
+ search_output = gr.Textbox(label="Search Results", lines=15)
698
+
699
+ search_btn.click(
700
+ test_search,
701
+ inputs=[search_manufacturer, search_product, search_article, search_power_min, search_power_max, search_type],
702
+ outputs=[search_output]
703
+ )
704
+
705
+ with gr.Tab("Get Details"):
706
+ with gr.Row():
707
+ details_article = gr.Textbox(label="Article Number", placeholder="Enter exact article number")
708
+ details_btn = gr.Button("Get Details")
709
+ details_output = gr.Textbox(label="Heat Pump Details", lines=20)
710
+ details_btn.click(test_get_details, inputs=[details_article], outputs=[details_output])
711
+
712
+ with gr.Tab("Data Management"):
713
+ with gr.Row():
714
+ with gr.Column():
715
+ manufacturers_btn = gr.Button("List Manufacturers")
716
+ manufacturers_output = gr.Textbox(label="Manufacturers", lines=10)
717
+ manufacturers_btn.click(test_list_manufacturers, outputs=[manufacturers_output])
718
+
719
+ with gr.Column():
720
+ summary_btn = gr.Button("Get Data Summary")
721
+ summary_output = gr.Textbox(label="Data Summary", lines=10)
722
+ summary_btn.click(test_get_summary, outputs=[summary_output])
723
+
724
+ # Following the official Gradio MCP pattern
725
+ demo.mcp_functions = {
726
+ "download_and_parse_part": {
727
+ "function": download_and_parse_part,
728
+ "description": "Download and automatically parse all VDI files for a part",
729
+ "parameters": {
730
+ "type": "object",
731
+ "properties": {
732
+ "part": {
733
+ "type": "integer",
734
+ "description": "Part number (e.g., 22 for heat pumps)"
735
+ }
736
+ },
737
+ "required": ["part"]
738
+ }
739
+ },
740
+ "search_heatpump": {
741
+ "function": search_heatpump,
742
+ "description": "Search for specific heat pump by criteria",
743
+ "parameters": {
744
+ "type": "object",
745
+ "properties": {
746
+ "manufacturer": {
747
+ "type": "string",
748
+ "description": "Manufacturer name (optional)"
749
+ },
750
+ "product_name": {
751
+ "type": "string",
752
+ "description": "Product name or partial match (optional)"
753
+ },
754
+ "article_number": {
755
+ "type": "string",
756
+ "description": "Article number (optional)"
757
+ },
758
+ "heating_power_min": {
759
+ "type": "number",
760
+ "description": "Minimum heating power in kW (optional)"
761
+ },
762
+ "heating_power_max": {
763
+ "type": "number",
764
+ "description": "Maximum heating power in kW (optional)"
765
+ },
766
+ "heat_pump_type": {
767
+ "type": "string",
768
+ "description": "Heat pump type (e.g., 'Luft-Wasser') (optional)"
769
+ }
770
+ }
771
+ }
772
+ },
773
+ "get_heatpump_details": {
774
+ "function": get_heatpump_details,
775
+ "description": "Get detailed information about a specific heat pump",
776
+ "parameters": {
777
+ "type": "object",
778
+ "properties": {
779
+ "article_number": {
780
+ "type": "string",
781
+ "description": "Article number of the heat pump"
782
+ }
783
+ },
784
+ "required": ["article_number"]
785
+ }
786
+ },
787
+ "list_manufacturers": {
788
+ "function": list_manufacturers,
789
+ "description": "List all available manufacturers in parsed data",
790
+ "parameters": {
791
+ "type": "object",
792
+ "properties": {}
793
+ }
794
+ },
795
+ "get_data_summary": {
796
+ "function": get_data_summary,
797
+ "description": "Get summary of all parsed heat pump data",
798
+ "parameters": {
799
+ "type": "object",
800
+ "properties": {}
801
+ }
802
+ }
803
+ }
804
+
805
+ return demo
806
 
807
+ # Main execution
808
  if __name__ == "__main__":
809
+ # Create and launch the Gradio interface
810
+ demo = create_interface()
 
 
 
 
 
811
 
812
+ # Launch with MCP server capability
 
 
 
 
 
 
813
  demo.launch(
814
+ server_name="0.0.0.0",
815
+ server_port=7860,
816
+ share=True,
817
+ mcp_server=True
818
+ )