Skip to content

Commit 8b220c1

Browse files
committed
enh: CSV + Regex für descriptor-Modul
- Füge CSV-Import über csvPath-Konfiguration hinzu - Implementiere Regex-Matching mit isRegex-Flag (YAML & CSV) - Erstelle unified cache für YAML- und CSV-Einträge - Wildcard-Replacement mit dynamische Beschreibungen - Erweitere Logging für bessere Debugging-Möglichkeiten Neue Features: * CSV-Dateien können parallel zu YAML-Beschreibungen verwendet werden * Regex-Unterstützung ermöglicht Pattern-basiertes Matching * Wildcards wie {TONE} werden in Beschreibungen ("add"-Werte) ersetzt * Vollständige Abwärtskompatibilität zu bestehenden Konfigurationen Technische Verbesserungen: * Unified cache-System für bessere Performance * Korrekte Iteration über Config-Objekte mit default-Parametern * Robuste Fehlerbehandlung für CSV-Import * continue statt break bei fehlenden scanFields Einschränkungen / known limitations: * Keine explizite Behandlung von Duplikaten * Standardverhalten ist „last one wins“, d. h. das zuletzt passende Descriptor-Objekt überschreibt den Wert * Wenn mehrere CSV/YAML denselben Schlüssel liefern, hängt das Ergebnis von Lade- bzw. Listen-Reihenfolge ab
1 parent a1cb545 commit 8b220c1

2 files changed

Lines changed: 222 additions & 17 deletions

File tree

docu/docs/modul/descriptor.md

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@ Informationen zum Aufbau eines [BOSWatch Pakets](../develop/packet.md)
2424
|descrField|Name des Feldes im BW Paket in welchem die Beschreibung gespeichert werden soll||
2525
|wildcard|Optional: Es kann für das angelegte `descrField` automatisch ein Wildcard registriert werden|None|
2626
|descriptions|Liste der Beschreibungen||
27+
|csvPath|Pfad der CSV-Datei (relativ zum Projektverzeichnis)||
2728

2829
#### `descriptions:`
2930
|Feld|Beschreibung|Default|
3031
|----|------------|-------|
3132
|for|Inhalt im `scanField` auf welchem geprüft werden soll||
3233
|add|Beschreibungstext welcher im `descrField` hinterlegt werden soll||
34+
|isRegex|Muss explizit auf `true` gesetzt werden, falls RegEx verwendet wird|false|
3335

3436
**Beispiel:**
3537
```yaml
@@ -44,6 +46,9 @@ Informationen zum Aufbau eines [BOSWatch Pakets](../develop/packet.md)
4446
add: FF DescriptorTest
4547
- for: '05678' # führende Nullen in '' !
4648
add: FF TestDescription
49+
- for: '890(1[1-9]|2[0-9])' # Regex-Pattern in '' !
50+
add: Feuerwehr Wache \\1 (BF)
51+
isRegex: true
4752
- scanField: status
4853
descrField: fmsStatDescr
4954
wildcard: "{STATUSTEXT}"
@@ -55,6 +60,62 @@ Informationen zum Aufbau eines [BOSWatch Pakets](../develop/packet.md)
5560
- ...
5661
```
5762
63+
**Wichtige Punkte für YAML-Regex:**
64+
- Apostroph: Regex-Pattern sollten in `'` stehen, um YAML-Parsing-Probleme zu vermeiden
65+
- isRegex-Flag: Muss explizit auf `true` gesetzt werden
66+
- Escaping: Backslashes müssen in YAML doppelt escaped werden (`\\1` statt `\1`)
67+
- Regex-Gruppen: Mit `\\1`, `\\2` etc. können Teile des Matches in der Beschreibung verwendet werden
68+
69+
#### `csvPath:`
70+
71+
**Beispiel:**
72+
```
73+
- type: module
74+
res: descriptor
75+
config:
76+
- scanField: tone
77+
descrField: description
78+
wildcard: "{DESCR}"
79+
csvPath: "config/descriptions_tone.csv"
80+
```
81+
82+
`csvPath` gibt den Pfad zu einer CSV-Datei an, relativ zum Projektverzeichnis (z. B. `"config/descriptions_tone.csv"`).
83+
84+
Eine neue CSV-Datei (z. B. `descriptions_tone.csv`) hat folgendes Format:
85+
86+
**Beispiel**
87+
```
88+
for,add,isRegex
89+
11111,KBI Landkreis Z,false
90+
12345,FF A-Dorf,false
91+
23456,FF B-Dorf,false
92+
^3456[0-9]$,FF Grossdorf, true
93+
```
94+
95+
In der Spalte isRegex kann **zusätzlich** angegeben werden, ob der Wert in for als regulärer Ausdruck interpretiert werden soll (true/false). Standardmäßig ist `false`.
96+
Wenn `isRegex` auf `true` gesetzt ist, wird der Wert aus `for` als regulärer Ausdruck ausgewertet.
97+
98+
### Kombination von YAML- und CSV-Konfiguration
99+
100+
Beide Varianten können parallel genutzt werden. In diesem Fall werden zuerst die Beschreibungen aus der YAML-Konfiguration und zusätzlich die Beschreibungen aus der angegebenen CSV-Datei geladen.
101+
102+
**Beispiel**
103+
104+
```
105+
- type: module
106+
res: descriptor
107+
config:
108+
- scanField: tone
109+
descrField: description
110+
wildcard: "{DESCR}"
111+
descriptions:
112+
- for: 12345
113+
add: FF YAML-Test
114+
- for: '05678' # führende Nullen in '' !
115+
add: FF YAML-Nullen
116+
csvPath: "config/descriptions_tone.csv"
117+
```
118+
58119
---
59120
## Modul Abhängigkeiten
60121
- keine
@@ -70,4 +131,4 @@ Informationen zum Aufbau eines [BOSWatch Pakets](../develop/packet.md)
70131

71132
---
72133
## Zusätzliche Wildcards
73-
- Von der Konfiguration abhängig
134+
- Von der Konfiguration abhängig

module/descriptor.py

Lines changed: 160 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@
1010
by Bastian Schroll
1111
1212
@file: descriptor.py
13-
@date: 27.10.2019
13+
@date: 04.08.2025
1414
@author: Bastian Schroll
15-
@description: Module to add descriptions to bwPackets
15+
@description: Module to add descriptions to bwPackets with CSV and Regex support
1616
"""
1717
import logging
18+
import csv
19+
import re
20+
import os
1821
from module.moduleBase import ModuleBase
1922

2023
# ###################### #
@@ -26,31 +29,172 @@
2629

2730

2831
class BoswatchModule(ModuleBase):
29-
r"""!Adds descriptions to bwPackets"""
32+
r"""!Adds descriptions to bwPackets with CSV and Regex support"""
3033
def __init__(self, config):
3134
r"""!Do not change anything here!"""
3235
super().__init__(__name__, config) # you can access the config class on 'self.config'
3336

3437
def onLoad(self):
3538
r"""!Called by import of the plugin"""
36-
for descriptor in self.config:
37-
if descriptor.get("wildcard", default=None):
38-
self.registerWildcard(descriptor.get("wildcard"), descriptor.get("descrField"))
39+
# Initialize unified cache for all descriptors
40+
self.unified_cache = {}
41+
42+
# Process each descriptor configuration
43+
for descriptor_config in self.config:
44+
scan_field = descriptor_config.get("scanField")
45+
descr_field = descriptor_config.get("descrField")
46+
descriptor_key = f"{scan_field}_{descr_field}"
47+
48+
# Register wildcard if specified
49+
if descriptor_config.get("wildcard", default=None):
50+
self.registerWildcard(descriptor_config.get("wildcard"), descr_field)
51+
52+
# Initialize cache for this descriptor
53+
self.unified_cache[descriptor_key] = []
54+
55+
# Load YAML descriptions first (for backward compatibility)
56+
yaml_descriptions = descriptor_config.get("descriptions", default=None)
57+
if yaml_descriptions:
58+
# yaml_descriptions is a Config object, we need to iterate properly
59+
for desc in yaml_descriptions:
60+
entry = {
61+
'for': str(desc.get("for", default="")),
62+
'add': desc.get("add", default=""),
63+
'isRegex': desc.get("isRegex", default=False) # Default: False
64+
}
65+
# Handle string 'true'/'false' values
66+
if isinstance(entry['isRegex'], str):
67+
entry['isRegex'] = entry['isRegex'].lower() == 'true'
68+
69+
self.unified_cache[descriptor_key].append(entry)
70+
logging.debug("Added YAML entry: %s -> %s", entry['for'], entry['add'])
71+
logging.info("Loaded %d YAML descriptions for %s", len(yaml_descriptions), descriptor_key)
72+
73+
# Load CSV descriptions if csvPath is specified
74+
csv_path = descriptor_config.get("csvPath", default=None)
75+
if csv_path:
76+
self._load_csv_data(csv_path, descriptor_key)
77+
78+
logging.info("Total entries for %s: %d", descriptor_key, len(self.unified_cache[descriptor_key]))
79+
80+
def _load_csv_data(self, csv_path, descriptor_key):
81+
r"""!Load CSV data for a descriptor and add to unified cache"""
82+
try:
83+
if not os.path.isfile(csv_path):
84+
logging.error("CSV file not found: %s", csv_path)
85+
return
86+
87+
csv_count = 0
88+
with open(csv_path, 'r', encoding='utf-8') as csvfile:
89+
reader = csv.DictReader(csvfile)
90+
for row in reader:
91+
# Set default values if columns are missing
92+
entry = {
93+
'for': str(row.get('for', '')),
94+
'add': row.get('add', ''),
95+
'isRegex': row.get('isRegex', 'false').lower() == 'true' # Default: False
96+
}
97+
self.unified_cache[descriptor_key].append(entry)
98+
csv_count += 1
99+
100+
logging.info("Loaded %d entries from CSV: %s for %s", csv_count, csv_path, descriptor_key)
101+
102+
except Exception as e:
103+
logging.error("Error loading CSV file %s: %s", csv_path, str(e))
104+
105+
def _find_description(self, descriptor_key, scan_value, bw_packet):
106+
r"""!Find matching description for a scan value with Regex group support."""
107+
descriptions = self.unified_cache.get(descriptor_key, [])
108+
scan_value_str = str(scan_value)
109+
110+
# Search for matching description
111+
for desc in descriptions:
112+
description_text = desc.get('add', '')
113+
match_pattern = desc.get('for', '')
114+
is_regex = desc.get('isRegex', False)
115+
116+
if is_regex:
117+
# Regex matching
118+
try:
119+
match = re.search(match_pattern, scan_value_str)
120+
if match:
121+
# Expand regex groups (\1, \2) in the description
122+
expanded_description = match.expand(description_text)
123+
124+
# Replace standard wildcards like {TONE}
125+
final_description = self._replace_wildcards(expanded_description, bw_packet)
126+
127+
logging.debug("Regex match '%s' -> '%s' for descriptor '%s'",
128+
match_pattern, final_description, descriptor_key)
129+
return final_description
130+
except re.error as e:
131+
logging.error("Invalid regex pattern '%s': %s", match_pattern, str(e))
132+
continue
133+
else:
134+
# Exact match
135+
if match_pattern == scan_value_str:
136+
# Replace standard wildcards like {TONE}
137+
final_description = self._replace_wildcards(description_text, bw_packet)
138+
logging.debug("Exact match '%s' -> '%s' for descriptor '%s'",
139+
match_pattern, final_description, descriptor_key)
140+
return final_description
141+
142+
return None
143+
144+
def _replace_wildcards(self, text, bw_packet):
145+
r"""!Replace all available wildcards in description text dynamically."""
146+
if not text or '{' not in text:
147+
return text
148+
149+
result = text
150+
151+
# Search for wildcards in the format {KEY} and replace them with values from the bw_packet
152+
found_wildcards = re.findall(r"\{([A-Z0-9_]+)\}", result)
153+
154+
for key in found_wildcards:
155+
key_lower = key.lower()
156+
value = bw_packet.get(key_lower)
157+
158+
if value is not None:
159+
result = result.replace(f"{{{key}}}", str(value))
160+
logging.debug("Replaced wildcard {%s} with value '%s'", key, value)
161+
162+
return result
39163

40164
def doWork(self, bwPacket):
41165
r"""!start an run of the module.
42166
43167
@param bwPacket: A BOSWatch packet instance"""
44-
for descriptor in self.config:
45-
if not bwPacket.get(descriptor.get("scanField")):
46-
break # scanField is not available in this packet
47-
bwPacket.set(descriptor.get("descrField"), bwPacket.get(descriptor.get("scanField")))
48-
for description in descriptor.get("descriptions"):
49-
if str(description.get("for")) == bwPacket.get(descriptor.get("scanField")):
50-
logging.debug("Description '%s' added in packet field '%s'",
51-
description.get("add"), descriptor.get("descrField"))
52-
bwPacket.set(descriptor.get("descrField"), description.get("add"))
53-
break # this descriptor has found a description - run next descriptor
168+
logging.debug("Processing packet with mode: %s", bwPacket.get("mode"))
169+
170+
# Process each descriptor configuration
171+
for descriptor_config in self.config:
172+
scan_field = descriptor_config.get("scanField")
173+
descr_field = descriptor_config.get("descrField")
174+
descriptor_key = f"{scan_field}_{descr_field}"
175+
176+
logging.debug("Processing descriptor: scanField='%s', descrField='%s'", scan_field, descr_field)
177+
178+
# Check if scanField is present in packet
179+
scan_value = bwPacket.get(scan_field)
180+
if scan_value is None:
181+
logging.debug("scanField '%s' not found in packet, skipping", scan_field)
182+
continue # scanField not available in this packet - try next descriptor
183+
184+
# Set default value (content of scanField)
185+
bwPacket.set(descr_field, str(scan_value))
186+
logging.debug("Set default value '%s' for field '%s'", scan_value, descr_field)
187+
188+
# Search for matching description in unified cache
189+
description = self._find_description(descriptor_key, scan_value, bwPacket)
190+
191+
if description:
192+
bwPacket.set(descr_field, description)
193+
logging.info("Description set: '%s' -> '%s'", scan_value, description)
194+
else:
195+
logging.debug("No description found for value '%s' in field '%s'", scan_value, scan_field)
196+
197+
logging.debug("Returning modified packet")
54198
return bwPacket
55199

56200
def onUnload(self):

0 commit comments

Comments
 (0)