File size: 34,220 Bytes
553bee0
 
4fd72e6
 
553bee0
 
4fd72e6
 
553bee0
 
4fd72e6
 
 
553bee0
 
 
 
 
4fd72e6
553bee0
4fd72e6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
553bee0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4fd72e6
553bee0
 
209fde9
4fd72e6
 
 
 
 
209fde9
 
4fd72e6
 
209fde9
 
553bee0
209fde9
 
 
4fd72e6
209fde9
 
 
 
 
 
 
 
 
 
 
 
4fd72e6
 
209fde9
 
 
 
 
 
 
 
553bee0
4fd72e6
 
 
 
 
 
 
 
 
209fde9
 
 
 
4fd72e6
 
553bee0
4fd72e6
553bee0
 
 
 
4fd72e6
553bee0
 
 
4fd72e6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
553bee0
 
 
4fd72e6
 
 
553bee0
4fd72e6
 
553bee0
 
 
 
4fd72e6
553bee0
4fd72e6
553bee0
 
4fd72e6
553bee0
 
4fd72e6
553bee0
 
4fd72e6
553bee0
 
4fd72e6
553bee0
 
 
4fd72e6
553bee0
 
 
4fd72e6
553bee0
 
 
4fd72e6
553bee0
 
 
4fd72e6
553bee0
4fd72e6
553bee0
 
 
 
 
 
 
4fd72e6
553bee0
 
 
 
 
4fd72e6
553bee0
 
 
 
 
 
 
4fd72e6
553bee0
 
 
4fd72e6
553bee0
 
 
4fd72e6
553bee0
4fd72e6
 
553bee0
4fd72e6
 
553bee0
4fd72e6
 
553bee0
 
4fd72e6
553bee0
 
 
 
 
 
 
 
4fd72e6
 
553bee0
 
 
 
 
 
 
 
4fd72e6
553bee0
 
 
 
 
4fd72e6
553bee0
 
 
 
 
 
 
 
4fd72e6
553bee0
 
 
 
 
 
 
 
 
 
 
 
4fd72e6
 
 
553bee0
4fd72e6
553bee0
4fd72e6
 
209fde9
553bee0
 
 
 
 
 
 
 
4fd72e6
553bee0
 
 
4fd72e6
553bee0
 
 
 
4fd72e6
 
553bee0
4fd72e6
553bee0
 
 
 
4fd72e6
 
553bee0
 
 
 
 
 
4fd72e6
553bee0
4fd72e6
 
 
553bee0
 
 
 
 
 
 
4fd72e6
553bee0
 
4fd72e6
 
553bee0
 
4fd72e6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
553bee0
 
4fd72e6
 
 
 
 
 
 
 
553bee0
 
4fd72e6
553bee0
 
 
209fde9
4fd72e6
553bee0
4fd72e6
553bee0
 
4fd72e6
553bee0
 
 
4fd72e6
 
 
 
 
 
 
 
 
553bee0
4fd72e6
553bee0
 
4fd72e6
553bee0
 
 
 
 
 
 
 
4fd72e6
 
 
 
 
 
553bee0
 
4fd72e6
553bee0
 
 
 
4fd72e6
553bee0
4fd72e6
553bee0
4fd72e6
 
553bee0
4fd72e6
 
 
 
 
 
553bee0
4fd72e6
 
 
553bee0
4fd72e6
 
 
 
 
 
553bee0
 
4fd72e6
 
 
 
 
553bee0
 
4fd72e6
553bee0
4fd72e6
2860196
4fd72e6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2860196
4fd72e6
553bee0
4fd72e6
 
209fde9
4fd72e6
553bee0
4fd72e6
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
#!/usr/bin/env python3
"""
Gradio MCP Server for VDI Heat Pump Data Parsing and Querying
Provides tools to download, parse VDI files and query specific heat pump information
"""

import asyncio
import json
import os
import sys
import pandas as pd
import numpy as np
from typing import Any, Dict, List, Optional
import zipfile
import tempfile
from pathlib import Path
import requests
import xml.etree.ElementTree as ET
import gradio as gr

# Add error logging
import logging
logging.basicConfig(level=logging.DEBUG)

# Import your existing parsing functions
def parse_010(r):
    """Parse 010:Vorlaufdaten"""
    return {
        'blatt': r[1] if len(r) > 1 else None,
        'hersteller': r[3] if len(r) > 3 else None,
        'datum': r[4] if len(r) > 4 else None,
    }

def parse_100(r):
    """Parse 100:Einsatzbereich"""
    return {
        'index_100': int(r[1]) if len(r) > 1 else None,
        'einsatzbereich': r[3] if len(r) > 3 else None,
    }

def parse_110(r):
    """Parse 110:Typ"""
    return {
        'index_110': int(r[1]) if len(r) > 1 else None,
        'typ': r[3] if len(r) > 3 else None,
    }

def parse_400(r):
    """Parse 400:Bauart"""
    return {
        'index_400': int(r[1]) if len(r) > 1 else None,
        'bauart': r[2] if len(r) > 2 else None,
    }

def parse_450(r):
    """Parse 450:Aufstellungsort"""
    return {
        'index_450': int(r[1]) if len(r) > 1 else None,
        'aufstellungsort': r[2] if len(r) > 2 else None,
    }

def parse_460(r):
    """Parse 460:Leistungsregelung der Wärmepumpe"""
    return {
        'index_460': int(r[1]) if len(r) > 1 else None,
        'leistungsregelung_der_wärmepumpe': r[2] if len(r) > 2 else None,
    }

def parse_700(r):
    """Parse 700:Produktelementdaten"""
    return {
        'index_700': int(r[1]) if len(r) > 1 else None,
        'sortiernummer': r[2] if len(r) > 2 else None,
        'produktname': r[3] if len(r) > 3 else None,
        'heizleistung': r[4] if len(r) > 4 else None,
        'leistungszahl': r[5] if len(r) > 5 else None,
        'elektrische_aufnahmeleistung_wärmepumpe': r[6] if len(r) > 6 else None,
        'leistung_der_elektrischen_zusatzheizung': r[7] if len(r) > 7 else None,
        'elektroanschluss': r[8] if len(r) > 8 else None,
        'anlaufstrom': r[9] if len(r) > 9 else None,
        'index_auf_satzart_200_wärmequelle': r[10] if len(r) > 10 else None,
        'eingesetztes_kältemittel': r[11] if len(r) > 11 else None,
        'füllmenge_des_kältemittels': r[12] if len(r) > 12 else None,
        'schallleistungspegel': r[13] if len(r) > 13 else None,
        'schutzart_nach_din_en_60529': r[14] if len(r) > 14 else None,
        'maximale_vorlauftemperatur': r[15] if len(r) > 15 else None,
        'heizwassertemperaturspreizung': r[16] if len(r) > 16 else None,
        'trinkwasser_erwärmung_über_indirekt_beheizten_speicher': r[17] if len(r) > 17 else None,
        'kühlfunktion': r[19] if len(r) > 19 else None,
        'kühlleistung': r[20] if len(r) > 20 else None,
        'verdichteranzahl': r[21] if len(r) > 21 else None,
        'leistungsstufen': r[22] if len(r) > 22 else None,
        'produktserie': r[32] if len(r) > 32 else None,
        'treibhauspotential_gwp': r[33] if len(r) > 33 else None,
        'bauart_des_kältekreis': r[35] if len(r) > 35 else None,
        'sicherheitsklasse_nach_din_en_378_1': r[36] if len(r) > 36 else None,
        'hinweistext_zum_kältemittel': r[37] if len(r) > 37 else None,
        'sanftanlasser': r[38] if len(r) > 38 else None,
    }

def parse_710_01(r):
    """Parse 710.01:Heizungs-Wärmepumpe"""
    return {
        'index_710_01': int(r[1]) if len(r) > 1 else None,
        'korrekturfaktor_7_k': r[2] if len(r) > 2 else None,
        'korrekturfaktor_10_k': r[3] if len(r) > 3 else None,
        'temperaturdifferenz_am_verflüssiger_bei_prüfstandmessung': r[4] if len(r) > 4 else None,
    }

def parse_710_04(r):
    """Parse 710.04:Wasser-Wasser-Wärmepumpe"""
    return {
        'index_710_04': int(r[1]) if len(r) > 1 else None,
        'leistungszahl_bei_w10_w35': r[2] if len(r) > 2 else None,
        'elektrische_leistungsaufnahme_wärmequellenpumpe': r[3] if len(r) > 3 else None,
        'heizleistung': r[4] if len(r) > 4 else None,
        'einsatzgrenze_wärmequelle_von': r[5] if len(r) > 5 else None,
        'einsatzgrenze_wärmequelle_bis': r[6] if len(r) > 6 else None,
        'volumenstrom_heizungsseitig': r[7] if len(r) > 7 else None,
        'wärmequellenpumpe_intern': r[8] if len(r) > 8 else None,
        'heizkreispumpe_intern': r[9] if len(r) > 9 else None,
        'elektrische_leistungsaufnahme_heizkreispumpe': r[10] if len(r) > 10 else None,
        'leistungszahl_bei_w10_w45': r[11] if len(r) > 11 else None,
        'leistungszahl_bei_w10_w55': r[12] if len(r) > 12 else None,
        'leistungszahl_bei_w7_w35': r[13] if len(r) > 13 else None,
        'kühlleistung_bei_w15_w23': r[14] if len(r) > 14 else None,
        'kühlleistungszahl_bei_w15_w23': r[15] if len(r) > 15 else None,
        'minimaler_volumenstrom_wärmequelle': r[16] if len(r) > 16 else None,
        'maximaler_volumenstrom_wärmequelle': r[17] if len(r) > 17 else None,
    }

def parse_710_05(r):
    """Parse 710.05:Luft-Wasser-Wärmepumpe"""
    return {
        'index_710_05': int(r[1]) if len(r) > 1 else None,
        'leistungszahl_bei_a_7_w35': r[2] if len(r) > 2 else None,
        'leistungszahl_bei_a2_w35': r[3] if len(r) > 3 else None,
        'leistungszahl_bei_a10_w35': r[4] if len(r) > 4 else None,
        'abtauart': r[5] if len(r) > 5 else None,
        'kühlfunktion_durch_kreislaufumkehr': r[6] if len(r) > 6 else None,
        'leistungszahl_bei_a7_w35': r[7] if len(r) > 7 else None,
        'leistungszahl_bei_a_15_w35': r[8] if len(r) > 8 else None,
        'leistungszahl_bei_a2_w45': r[9] if len(r) > 9 else None,
        'leistungszahl_bei_a7_w45': r[10] if len(r) > 10 else None,
        'leistungszahl_bei_a_7_w55': r[11] if len(r) > 11 else None,
        'leistungszahl_bei_a7_w55': r[12] if len(r) > 12 else None,
        'leistungszahl_bei_a10_w55': r[13] if len(r) > 13 else None,
        'kühlleistungszahl_bei_a35_w7': r[14] if len(r) > 14 else None,
        'kühlleistungszahl_bei_a35_w18': r[15] if len(r) > 15 else None,
        'kühlleistung_bei_a35_w7': r[16] if len(r) > 16 else None,
        'kühlleistung_bei_a35_w18': r[17] if len(r) > 17 else None,
        'minimale_einsatzgrenze_wärmequelle': r[18] if len(r) > 18 else None,
        'maximale_einsatzgrenze_wärmequelle': r[19] if len(r) > 19 else None,
        'leistungszahl_a20_w35': r[20] if len(r) > 20 else None,
        'leistungszahl_a20_w45': r[21] if len(r) > 21 else None,
        'leistungszahl_a20_w55': r[22] if len(r) > 22 else None,
        'leistungsaufnahme_luefter': r[25] if len(r) > 25 else None,
        'volumenstrom_heizungsseitig': r[26] if len(r) > 26 else None,
    }

def parse_710_07(r):
    """Parse 710.07:Einbringmaße"""
    return {
        'index_710_07': int(r[1]) if len(r) > 1 else None,
        'art_der_maße': r[2] if len(r) > 2 else None,
        'länge': r[3] if len(r) > 3 else None,
        'breite': r[4] if len(r) > 4 else None,
        'höhe': r[5] if len(r) > 5 else None,
        'masse': r[6] if len(r) > 6 else None,
        'beschreibung': r[7] if len(r) > 7 else None,
    }

def parse_800(r):
    """Parse 800:TGA-Nummer"""
    return {
        'index_800': int(r[1]) if len(r) > 1 else None,
        'tga_nummer': r[2] if len(r) > 2 else None,
    }

def parse_810(r):
    """Parse 810:Artikelnummern"""
    return {
        'index_810': int(r[1]) if len(r) > 1 else None,
        'artikelnummer': r[2] if len(r) > 2 else None,
        'artikelname': r[9] if len(r) > 9 else None,
        'energieeffizienzklasse': r[10] if len(r) > 10 else None,
        'erp_richtlinie': r[11] if len(r) > 11 else None,
    }

class VDIHeatPumpParser:
    """Main parser class for VDI heat pump data"""
    
    def __init__(self):
        self.data_cache = {}
        self.parsed_files = {}
        self.domain = "bim4hvac.com"
    
    def check_catalogs_for_part(self, part: int) -> str:
        """Check if catalogs have been updated for a part"""
        try:
            url = f"http://catalogue.{self.domain}/bdh/ws/mcc.asmx"
            payload = f"""<?xml version="1.0" encoding="utf-8"?>
            <soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
            <soap:Body>
                <CheckCatalogsForPart xmlns="urn:bdhmcc">
                <part>{part}</part>
                </CheckCatalogsForPart>
            </soap:Body>
            </soap:Envelope>
            """
            headers = {'Content-Type': 'text/xml; charset=utf-8'}
            response = requests.post(url, headers=headers, data=payload, timeout=30)
            return response.text
        except Exception as e:
            raise Exception(f"Error checking catalogs for part {part}: {str(e)}")
    
    def download_vdi_files(self, part: int, download_dir: str = None) -> List[str]:
        """Download all VDI files for a given part"""
        try:
            if download_dir is None:
                download_dir = os.path.join(tempfile.gettempdir(), f"vdi_part{part:02d}")
            
            os.makedirs(download_dir, exist_ok=True)
            
            # Get catalog information
            catalog_xml = self.check_catalogs_for_part(part)
            
            # Parse the XML response
            root = ET.fromstring(catalog_xml)
            result = root.find('.//{urn:bdhmcc}CheckCatalogsForPartResult')
            
            if result is None:
                raise Exception("No catalog results found")
            
            # Parse the catalog data
            catalogs_xml = ET.fromstring(result.text)
            
            # Extract catalog information
            data = []
            for catalog in catalogs_xml.findall('Catalog'):
                data.append({
                    'mfr': catalog.get('mfr'),
                    'nick': catalog.get('nick').split('/')[0].strip(),
                    'part': f"{int(catalog.get('part')):02d}",
                })

            df = pd.DataFrame(data)
            
            # Build download URLs
            df['slug'] = df['nick'] + df['part']
            df['download_url'] = f"https://www.catalogue.{self.domain}/" + df['slug'] + "/Downloads/PART" + df['part'] + "_" + df['nick'] + ".zip"
            
            # Download files
            downloaded_files = []
            for _, row in df.iterrows():
                download_url = row['download_url']
                file_name = os.path.join(download_dir, f"PART{row['part']}_{row['nick']}.zip")
                
                print(f"Downloading {download_url}...")
                response = requests.get(download_url, timeout=30)
                response.raise_for_status()
                
                with open(file_name, 'wb') as file:
                    file.write(response.content)
                
                downloaded_files.append(file_name)
                print(f"Downloaded: {file_name}")
            
            return downloaded_files
            
        except Exception as e:
            raise Exception(f"Error downloading VDI files for part {part}: {str(e)}")
    
    def parse_vdi_file(self, file_path: str, nick: str) -> pd.DataFrame:
        """Parse a single VDI file using the full parsing logic"""
        try:
            with open(file_path, 'r', encoding="latin-1") as file:
                text = file.read()
            
            # Read VDI file into lines
            lines = np.array(text.split("\n"))
            records = list(map(lambda x: x.split(";"), lines))
            
            # Initialize hierarchical records
            r010 = parse_010([])
            r100 = parse_100([])
            r110 = parse_110([])
            r400 = parse_400([])
            r450 = parse_450([])
            r460 = parse_460([])
            r700 = parse_700([])
            r710_01 = parse_710_01([])
            r710_04 = parse_710_04([])
            r710_05 = parse_710_05([])
            r710_07 = parse_710_07([])
            r800 = parse_800([])
            r810 = parse_810([])

            # Parse records
            data = []
            for r in records:
                if r[0] == '010':
                    r010 = parse_010(r)
                if r[0] == '100':
                    r100 = parse_100(r)
                elif r[0] == '110':
                    r110 = parse_110(r)
                    # keep track of 400s and 700s for merging with 800s
                    r400s = []
                    r450s = []
                    r460s = []
                    r700s = []
                    r710_07s = {}  # capture list of 710.07 records by index_700
                elif r[0] == '400':
                    r400 = parse_400(r)
                    r400s.append(r400)
                elif r[0] == '450':
                    r450 = parse_450(r)
                    r450s.append(r450)
                elif r[0] == '460':
                    r460 = parse_460(r)
                    r460s.append(r460)
                elif r[0] == '700':
                    r700 = parse_700(r)
                    r700s.append(r700)
                elif r[0] == '710.01':
                    r710_01 = parse_710_01(r)
                    if r700s:
                        r700s[-1].update(r710_01)
                elif r[0] == '710.04':
                    r710_04 = parse_710_04(r)
                    if r700s:
                        r700s[-1].update(r710_04)
                elif r[0] == '710.05':
                    r710_05 = parse_710_05(r)
                    if r700s:
                        r700s[-1].update(r710_05)
                elif r[0] == '710.07':
                    r710_07 = parse_710_07(r)
                    if r700s:
                        r710_07s.setdefault(r700s[-1]['index_700'], []).append(r710_07)
                elif r[0] == '800':
                    r800 = parse_800(r)
                    
                    # deconstruct indexes from TGA number
                    if r800['tga_nummer'] and len(r800['tga_nummer']) >= 50:
                        try:
                            index_400 = int(r800['tga_nummer'][27:30])
                            index_450 = int(r800['tga_nummer'][30:33])
                            index_460 = int(r800['tga_nummer'][33:36])
                            index_700 = int(r800['tga_nummer'][45:50])
                            
                            # get corresponding records
                            r400 = next((r for r in r400s if r['index_400'] == index_400), {})
                            r450 = next((r for r in r450s if r['index_450'] == index_450), {})
                            r460 = next((r for r in r460s if r['index_460'] == index_460), {})
                            r700 = next((r for r in r700s if r['index_700'] == index_700), {})
                            
                            # pick measurement record 710.07 (2=Aufstellmaße)
                            filtered_r710_07s = [r for r in r710_07s.get(index_700, []) if r['art_der_maße'] == '2']
                            if len(filtered_r710_07s) == 1:
                                r710_07 = filtered_r710_07s[0]
                            else:
                                aufstellungsort = r450.get('aufstellungsort', '').lower()
                                r710_07 = next((r for r in filtered_r710_07s if r.get('beschreibung', '').lower().startswith(aufstellungsort)), {})
                        except (ValueError, IndexError):
                            # Handle malformed TGA numbers
                            r400, r450, r460, r700, r710_07 = {}, {}, {}, {}, {}
                            
                elif r[0] == '810':
                    r810 = parse_810(r)
                    if r700:
                        data.append({**r810, **r800, **r710_07, **r700, **r460, **r450, **r400, **r110, **r100, **r010})

            # Create DataFrame
            df = pd.DataFrame(data)
            
            # Add nick
            df['nick'] = nick
            
            # Remove newlines in all cells
            df = df.replace({r'[\r\n]+': ' '}, regex=True)
            
            # Replace ¶ with comma
            df = df.replace({r'¶': ', '}, regex=True)
            
            # Cast all columns starting with 'index_' as integer
            index_columns = [col for col in df.columns if col.startswith('index_')]
            for col in index_columns:
                if col in df.columns:
                    df[col] = df[col].replace('', pd.NA).astype('Int64')
            
            return df
            
        except Exception as e:
            raise Exception(f"Error parsing VDI file {file_path}: {str(e)}")
        
    def extract_and_parse_zip(self, zip_path: str) -> pd.DataFrame:
        """Extract ZIP file and parse VDI files"""
        nick = Path(zip_path).stem.split('_')[-1]
        
        with tempfile.TemporaryDirectory() as temp_dir:
            with zipfile.ZipFile(zip_path, 'r') as zip_ref:
                zip_ref.extractall(temp_dir)
            
            # Find VDI files recursively
            vdi_files = list(Path(temp_dir).rglob("*.vdi"))
            if not vdi_files:
                vdi_files = list(Path(temp_dir).rglob("*.VDI"))
            
            if not vdi_files:
                raise Exception(f"No VDI files found in {zip_path}")
            
            all_data = []
            for vdi_file in vdi_files:
                try:
                    df = self.parse_vdi_file(str(vdi_file), nick)
                    if not df.empty:
                        all_data.append(df)
                except Exception as e:
                    print(f"Error parsing {vdi_file.name}: {e}")
                    continue
            
            if all_data:
                combined_df = pd.concat(all_data, ignore_index=True)
                self.parsed_files[zip_path] = combined_df
                return combined_df
            
            return pd.DataFrame()

# Initialize the parser
parser = VDIHeatPumpParser()

# MCP Tool Functions
def download_and_parse_part(part: int) -> str:
    """Download and parse VDI files for a specific part"""
    try:
        download_dir = os.path.join(tempfile.gettempdir(), f"vdi_part{part:02d}")
        
        # Download files
        downloaded_files = parser.download_vdi_files(part, download_dir)
        
        # Parse all downloaded files
        all_data = []
        for file_path in downloaded_files:
            try:
                df = parser.extract_and_parse_zip(file_path)
                if not df.empty:
                    all_data.append(df)
            except Exception as e:
                print(f"Error parsing {file_path}: {e}")
        
        if all_data:
            combined_df = pd.concat(all_data, ignore_index=True)
            cache_key = f"part_{part}"
            parser.parsed_files[cache_key] = combined_df
            
            result = {
                "status": "success",
                "message": f"Successfully downloaded and parsed {len(combined_df)} heat pump records for part {part}",
                "part": part,
                "record_count": len(combined_df),
                "manufacturers": sorted(combined_df['hersteller'].dropna().unique().tolist())
            }
        else:
            result = {
                "status": "warning",
                "message": f"No heat pump data found for part {part}",
                "part": part,
                "record_count": 0
            }
        
        return json.dumps(result, indent=2)
        
    except Exception as e:
        return json.dumps({"status": "error", "message": f"Error: {str(e)}"}, indent=2)

def search_heatpump(manufacturer: str = None, product_name: str = None, article_number: str = None,
                   heating_power_min: float = None, heating_power_max: float = None, heat_pump_type: str = None) -> str:
    """Search for heat pumps based on criteria"""
    try:
        # Combine all parsed data
        all_data = []
        for df in parser.parsed_files.values():
            all_data.append(df)
        
        if not all_data:
            return json.dumps({"status": "error", "message": "No data available. Please parse VDI files first."})
        
        combined_df = pd.concat(all_data, ignore_index=True)
        
        # Apply filters
        filtered_df = combined_df.copy()
        
        if manufacturer:
            filtered_df = filtered_df[
                filtered_df['hersteller'].str.contains(manufacturer, case=False, na=False)
            ]
        
        if product_name:
            filtered_df = filtered_df[
                filtered_df['produktname'].str.contains(product_name, case=False, na=False)
            ]
        
        if article_number:
            filtered_df = filtered_df[
                filtered_df['artikelnummer'].str.contains(article_number, case=False, na=False)
            ]
        
        if heat_pump_type:
            filtered_df = filtered_df[
                filtered_df['typ'].str.contains(heat_pump_type, case=False, na=False)
            ]
        
        # Filter by heating power
        if heating_power_min is not None or heating_power_max is not None:
            filtered_df['heizleistung_numeric'] = pd.to_numeric(filtered_df['heizleistung'], errors='coerce')
            
            if heating_power_min is not None:
                filtered_df = filtered_df[filtered_df['heizleistung_numeric'] >= heating_power_min]
            
            if heating_power_max is not None:
                filtered_df = filtered_df[filtered_df['heizleistung_numeric'] <= heating_power_max]
        
        # Return results
        result_columns = ['hersteller', 'produktname', 'artikelnummer', 'heizleistung', 'typ', 'energieeffizienzklasse']
        available_columns = [col for col in result_columns if col in filtered_df.columns]
        result_df = filtered_df[available_columns].head(20)  # Limit to 20 results
        
        return result_df.to_json(orient='records', indent=2)
        
    except Exception as e:
        return json.dumps({"status": "error", "message": f"Error searching heat pumps: {str(e)}"}, indent=2)

def get_heatpump_details(article_number: str) -> str:
    """Get detailed information about a specific heat pump"""
    try:
        # Search across all parsed data
        for df in parser.parsed_files.values():
            matching_rows = df[df['artikelnummer'] == article_number]
            if not matching_rows.empty:
                details = matching_rows.iloc[0].to_dict()
                # Clean up None values
                details = {k: v for k, v in details.items() if v is not None and v != ''}
                return json.dumps(details, indent=2, default=str)
        
        return json.dumps({"status": "error", "message": f"Heat pump with article number '{article_number}' not found"})
        
    except Exception as e:
        return json.dumps({"status": "error", "message": f"Error getting heat pump details: {str(e)}"}, indent=2)

def list_manufacturers() -> str:
    """List all manufacturers in the parsed data"""
    try:
        all_manufacturers = set()
        for df in parser.parsed_files.values():
            if 'hersteller' in df.columns:
                manufacturers = df['hersteller'].dropna().unique()
                all_manufacturers.update(manufacturers)
        
        if not all_manufacturers:
            return json.dumps({
                "status": "warning",
                "message": "No manufacturers available. Please parse VDI files first.",
                "manufacturers": [],
                "count": 0
            })
        
        result = {
            "status": "success",
            "manufacturers": sorted(list(all_manufacturers)),
            "count": len(all_manufacturers)
        }
        return json.dumps(result, indent=2)
        
    except Exception as e:
        return json.dumps({"status": "error", "message": f"Error listing manufacturers: {str(e)}"}, indent=2)

def get_data_summary() -> str:
    """Get summary statistics of all parsed data"""
    try:
        if not parser.parsed_files:
            return json.dumps({
                "status": "warning",
                "message": "No data available. Please parse VDI files first.",
                "total_records": 0
            })
        
        total_records = 0
        manufacturers = set()
        heat_pump_types = set()
        
        for df in parser.parsed_files.values():
            total_records += len(df)
            if 'hersteller' in df.columns:
                manufacturers.update(df['hersteller'].dropna().unique())
            if 'typ' in df.columns:
                heat_pump_types.update(df['typ'].dropna().unique())
        
        result = {
            "status": "success",
            "total_records": total_records,
            "manufacturer_count": len(manufacturers),
            "heat_pump_types": sorted(list(heat_pump_types)),
            "parsed_files": len(parser.parsed_files)
        }
        return json.dumps(result, indent=2)
        
    except Exception as e:
        return json.dumps({"status": "error", "message": f"Error getting data summary: {str(e)}"}, indent=2)

# Create Gradio interface following the official MCP pattern
def create_interface():
    """Create Gradio interface with MCP server"""
    
    def test_download_and_parse(part_number):
        """Test the download and parse functionality"""
        if not part_number:
            return "Please enter a part number"
        
        try:
            part = int(part_number)
            result = download_and_parse_part(part)
            return result
        except ValueError:
            return "Please enter a valid integer for part number"
        except Exception as e:
            return f"Error: {str(e)}"
    
    def test_search(manufacturer, product_name, article_number, heating_power_min, heating_power_max, heat_pump_type):
        """Test the search functionality"""
        try:
            # Convert empty strings to None
            manufacturer = manufacturer if manufacturer.strip() else None
            product_name = product_name if product_name.strip() else None
            article_number = article_number if article_number.strip() else None
            heat_pump_type = heat_pump_type if heat_pump_type.strip() else None
            
            # Convert power values
            heating_power_min = float(heating_power_min) if heating_power_min else None
            heating_power_max = float(heating_power_max) if heating_power_max else None
            
            result = search_heatpump(
                manufacturer=manufacturer,
                product_name=product_name,
                article_number=article_number,
                heating_power_min=heating_power_min,
                heating_power_max=heating_power_max,
                heat_pump_type=heat_pump_type
            )
            return result
        except Exception as e:
            return f"Error: {str(e)}"
    
    def test_get_details(article_number):
        """Test getting heat pump details"""
        if not article_number.strip():
            return "Please enter an article number"
        
        result = get_heatpump_details(article_number.strip())
        return result
    
    def test_list_manufacturers():
        """Test listing manufacturers"""
        result = list_manufacturers()
        return result
    
    def test_get_summary():
        """Test getting data summary"""
        result = get_data_summary()
        return result
    
    # Create Gradio interface
    with gr.Blocks(title="VDI Heat Pump Parser - MCP Server") as demo:
        gr.Markdown("# VDI Heat Pump Parser - MCP Server")
        gr.Markdown("This interface allows you to test the MCP tools for parsing VDI heat pump data.")
        
        with gr.Tab("Download & Parse"):
            with gr.Row():
                part_input = gr.Textbox(label="Part Number", placeholder="Enter part number (e.g., 22 for heat pumps)")
                download_btn = gr.Button("Download & Parse")
            download_output = gr.Textbox(label="Result", lines=10)
            download_btn.click(test_download_and_parse, inputs=[part_input], outputs=[download_output])
        
        with gr.Tab("Search Heat Pumps"):
            with gr.Row():
                with gr.Column():
                    search_manufacturer = gr.Textbox(label="Manufacturer", placeholder="e.g., Viessmann")
                    search_product = gr.Textbox(label="Product Name", placeholder="e.g., Vitocal")
                    search_article = gr.Textbox(label="Article Number", placeholder="e.g., 12345")
                with gr.Column():
                    search_power_min = gr.Textbox(label="Min Heating Power (kW)", placeholder="e.g., 5")
                    search_power_max = gr.Textbox(label="Max Heating Power (kW)", placeholder="e.g., 15")
                    search_type = gr.Textbox(label="Heat Pump Type", placeholder="e.g., Luft-Wasser")
            
            search_btn = gr.Button("Search")
            search_output = gr.Textbox(label="Search Results", lines=15)
            
            search_btn.click(
                test_search,
                inputs=[search_manufacturer, search_product, search_article, search_power_min, search_power_max, search_type],
                outputs=[search_output]
            )
        
        with gr.Tab("Get Details"):
            with gr.Row():
                details_article = gr.Textbox(label="Article Number", placeholder="Enter exact article number")
                details_btn = gr.Button("Get Details")
            details_output = gr.Textbox(label="Heat Pump Details", lines=20)
            details_btn.click(test_get_details, inputs=[details_article], outputs=[details_output])
        
        with gr.Tab("Data Management"):
            with gr.Row():
                with gr.Column():
                    manufacturers_btn = gr.Button("List Manufacturers")
                    manufacturers_output = gr.Textbox(label="Manufacturers", lines=10)
                    manufacturers_btn.click(test_list_manufacturers, outputs=[manufacturers_output])
                
                with gr.Column():
                    summary_btn = gr.Button("Get Data Summary")
                    summary_output = gr.Textbox(label="Data Summary", lines=10)
                    summary_btn.click(test_get_summary, outputs=[summary_output])
    
    # Following the official Gradio MCP pattern
    demo.mcp_functions = {
        "download_and_parse_part": {
            "function": download_and_parse_part,
            "description": "Download and automatically parse all VDI files for a part",
            "parameters": {
                "type": "object",
                "properties": {
                    "part": {
                        "type": "integer",
                        "description": "Part number (e.g., 22 for heat pumps)"
                    }
                },
                "required": ["part"]
            }
        },
        "search_heatpump": {
            "function": search_heatpump,
            "description": "Search for specific heat pump by criteria",
            "parameters": {
                "type": "object",
                "properties": {
                    "manufacturer": {
                        "type": "string",
                        "description": "Manufacturer name (optional)"
                    },
                    "product_name": {
                        "type": "string",
                        "description": "Product name or partial match (optional)"
                    },
                    "article_number": {
                        "type": "string",
                        "description": "Article number (optional)"
                    },
                    "heating_power_min": {
                        "type": "number",
                        "description": "Minimum heating power in kW (optional)"
                    },
                    "heating_power_max": {
                        "type": "number",
                        "description": "Maximum heating power in kW (optional)"
                    },
                    "heat_pump_type": {
                        "type": "string",
                        "description": "Heat pump type (e.g., 'Luft-Wasser') (optional)"
                    }
                }
            }
        },
        "get_heatpump_details": {
            "function": get_heatpump_details,
            "description": "Get detailed information about a specific heat pump",
            "parameters": {
                "type": "object",
                "properties": {
                    "article_number": {
                        "type": "string",
                        "description": "Article number of the heat pump"
                    }
                },
                "required": ["article_number"]
            }
        },
        "list_manufacturers": {
            "function": list_manufacturers,
            "description": "List all available manufacturers in parsed data",
            "parameters": {
                "type": "object",
                "properties": {}
            }
        },
        "get_data_summary": {
            "function": get_data_summary,
            "description": "Get summary of all parsed heat pump data",
            "parameters": {
                "type": "object",
                "properties": {}
            }
        }
    }
    
    return demo

# Main execution
if __name__ == "__main__":
    # Create and launch the Gradio interface
    demo = create_interface()
    
    # Launch with MCP server capability
    demo.launch(
        server_name="0.0.0.0",
        server_port=7860,
        share=True,
        mcp_server=True
    )