diff --git a/docu/docs/modul/descriptor.md b/docu/docs/modul/descriptor.md index 43444d3..7eb62e4 100644 --- a/docu/docs/modul/descriptor.md +++ b/docu/docs/modul/descriptor.md @@ -2,9 +2,9 @@ --- ## Beschreibung -Mit diesem Modul können einem Alarmpaket beliebige Beschreibungen in Abhänigkeit der enthaltenen Informationen hinzugefügt werden. +Mit diesem Modul können einem Alarmpaket beliebige Beschreibungen in Abhängigkeit der enthaltenen Informationen hinzugefügt werden. -## Unterstütze Alarmtypen +## Unterstützte Alarmtypen - Fms - Pocsag - Zvei @@ -16,20 +16,22 @@ Mit diesem Modul können einem Alarmpaket beliebige Beschreibungen in Abhänigke ## Konfiguration Informationen zum Aufbau eines [BOSWatch Pakets](../develop/packet.md) -**Achtung:** Zahlen welche führende Nullen entahlten müssen in Anführungszeichen gesetzt werden Bsp. `'0012345'` +**Achtung:** Zahlen, die führende Nullen enthalten, müssen in der YAML-Konfiguration in Anführungszeichen gesetzt werden, z.B. `'0012345'`. In CSV-Dateien ist dies nicht zwingend erforderlich. |Feld|Beschreibung|Default| |----|------------|-------| -|scanField|Feld des BW Pakets welches geprüft werden soll|| -|descrField|Name des Feldes im BW Paket in welchem die Beschreibung gespeichert werden soll|| +|scanField|Feld des BW Pakets, welches geprüft werden soll|| +|descrField|Name des Feldes im BW Paket, in welchem die Beschreibung gespeichert werden soll|| |wildcard|Optional: Es kann für das angelegte `descrField` automatisch ein Wildcard registriert werden|None| |descriptions|Liste der Beschreibungen|| +|csvPath|Pfad der CSV-Datei (relativ zum Projektverzeichnis)|| #### `descriptions:` |Feld|Beschreibung|Default| |----|------------|-------| -|for|Inhalt im `scanField` auf welchem geprüft werden soll|| -|add|Beschreibungstext welcher im `descrField` hinterlegt werden soll|| +|for|Wert im `scanField`, der geprüft werden soll|| +|add|Beschreibungstext, der im `descrField` hinterlegt werden soll|| +|isRegex|Muss explizit auf `true` gesetzt werden, wenn RegEx verwendet wird|false| **Beispiel:** ```yaml @@ -44,6 +46,9 @@ Informationen zum Aufbau eines [BOSWatch Pakets](../develop/packet.md) add: FF DescriptorTest - for: '05678' # führende Nullen in '' ! add: FF TestDescription + - for: '890(1[1-9]|2[0-9])' # Regex-Pattern in '' ! + add: Feuerwehr Wache \\1 (BF) + isRegex: true - scanField: status descrField: fmsStatDescr wildcard: "{STATUSTEXT}" @@ -55,6 +60,70 @@ Informationen zum Aufbau eines [BOSWatch Pakets](../develop/packet.md) - ... ``` +**Wichtige Punkte für YAML-Regex:** +- Apostroph: Regex-Pattern sollten in `'` stehen, um YAML-Parsing-Probleme zu vermeiden +- isRegex-Flag: Muss explizit auf `true` gesetzt werden +- Escaping: Backslashes müssen in YAML doppelt escaped werden (`\\1` statt `\1`) +- Regex-Gruppen: Mit `\\1`, `\\2` etc. können Teile des Matches in der Beschreibung verwendet werden + +#### `csvPath:` + +**Beispiel:** +``` +- type: module + res: descriptor + config: + - scanField: tone + descrField: description + wildcard: "{DESCR}" + csvPath: "config/descriptions_tone.csv" +``` + +`csvPath` gibt den Pfad zu einer CSV-Datei an, relativ zum Projektverzeichnis (z. B. `"config/descriptions_tone.csv"`). + +Eine neue CSV-Datei (z. B. `descriptions_tone.csv`) hat folgendes Format: + +**Beispiel** +``` +for,add,isRegex +11111,KBI Landkreis Z,false +12345,FF A-Dorf,false +23456,FF B-Dorf +^3456[0-9]$,FF Grossdorf,true +``` + +**Hinweis:** In CSV-Dateien müssen Werte mit führenden Nullen **nicht** in Anführungszeichen gesetzt werden (können aber, falls gewünscht). Die Spalte `isRegex` gibt an, ob der Wert in `for` als regulärer Ausdruck interpretiert werden soll (true/false). Falls kein Wert angegeben ist, ist die Paarung standardmäßig `false`, wie z.B. beim Eintrag der FF B-Dorf im obigen Beispiel. + +### Kombination von YAML- und CSV-Konfiguration + +Beide Varianten können parallel genutzt werden. In diesem Fall werden die Beschreibungen aus der YAML-Konfiguration und aus der angegebenen CSV-Datei in einer gemeinsamen Datenbank zusammengeführt. + +#### Matching-Reihenfolge und Priorität + +Das Modul wendet folgende Prioritäten beim Matching an: + +1. **Exakte Matches** (aus YAML und CSV) werden zuerst geprüft +2. **Regex-Matches** (aus YAML und CSV) werden nur geprüft, wenn kein exakter Match gefunden wurde + +**Beispiel für Kombination:** +```yaml +- type: module + res: descriptor + config: + - scanField: tone + descrField: description + wildcard: "{DESCR}" + descriptions: + - for: 12345 + add: FF YAML-Test (exakt) + - for: '123.*' + add: FF YAML-Regex + isRegex: true + csvPath: "config/descriptions_tone.csv" +``` + +Bei einem `scanField`-Wert von `12345` wird **immer** "FF YAML-Test (exakt)" verwendet, auch wenn der Regex ebenfalls zutreffen würde. Regex-Matches werden nur verwendet, wenn kein exakter Match gefunden wurde - unabhängig davon, ob die Einträge aus YAML oder CSV stammen. + --- ## Modul Abhängigkeiten - keine @@ -70,4 +139,4 @@ Informationen zum Aufbau eines [BOSWatch Pakets](../develop/packet.md) --- ## Zusätzliche Wildcards -- Von der Konfiguration abhängig +- Von der Konfiguration abhängig \ No newline at end of file diff --git a/module/descriptor.py b/module/descriptor.py index 132384a..eeda938 100644 --- a/module/descriptor.py +++ b/module/descriptor.py @@ -10,11 +10,14 @@ r"""! by Bastian Schroll @file: descriptor.py -@date: 27.10.2019 +@date: 03.12.2025 @author: Bastian Schroll -@description: Module to add descriptions to bwPackets +@description: Module to add descriptions to bwPackets with CSV and Regex support """ import logging +import csv +import re +import os from module.moduleBase import ModuleBase # ###################### # @@ -26,31 +29,191 @@ logging.debug("- %s loaded", __name__) class BoswatchModule(ModuleBase): - r"""!Adds descriptions to bwPackets""" + r"""!Adds descriptions to bwPackets with CSV and Regex support""" def __init__(self, config): r"""!Do not change anything here!""" super().__init__(__name__, config) # you can access the config class on 'self.config' def onLoad(self): r"""!Called by import of the plugin""" - for descriptor in self.config: - if descriptor.get("wildcard", default=None): - self.registerWildcard(descriptor.get("wildcard"), descriptor.get("descrField")) + # Initialize unified cache for all descriptors + self.unified_cache = {} + + # Process each descriptor configuration + for descriptor_config in self.config: + scan_field = descriptor_config.get("scanField") + descr_field = descriptor_config.get("descrField") + descriptor_key = f"{scan_field}_{descr_field}" + + # Register wildcard if specified + if descriptor_config.get("wildcard", default=None): + self.registerWildcard(descriptor_config.get("wildcard"), descr_field) + + # Initialize cache for this descriptor + self.unified_cache[descriptor_key] = [] + + # Load YAML descriptions first (for backward compatibility) + yaml_descriptions = descriptor_config.get("descriptions", default=None) + if yaml_descriptions: + # yaml_descriptions is a Config object, we need to iterate properly + for desc in yaml_descriptions: + entry = { + 'for': str(desc.get("for", default="")), + 'add': desc.get("add", default=""), + 'isRegex': desc.get("isRegex", default=False) # Default: False + } + # Handle string 'true'/'false' values + if isinstance(entry['isRegex'], str): + entry['isRegex'] = entry['isRegex'].lower() == 'true' + + self.unified_cache[descriptor_key].append(entry) + logging.debug("Added YAML entry: %s -> %s", entry['for'], entry['add']) + logging.info("Loaded %d YAML descriptions for %s", len(yaml_descriptions), descriptor_key) + + # Load CSV descriptions if csvPath is specified + csv_path = descriptor_config.get("csvPath", default=None) + if csv_path: + self._load_csv_data(csv_path, descriptor_key) + + logging.info("Total entries for %s: %d", descriptor_key, len(self.unified_cache[descriptor_key])) + + def _load_csv_data(self, csv_path, descriptor_key): + r"""!Load CSV data for a descriptor and add to unified cache""" + try: + if not os.path.isfile(csv_path): + logging.error("CSV file not found: %s", csv_path) + return + + csv_count = 0 + with open(csv_path, 'r', encoding='utf-8') as csvfile: + reader = csv.DictReader(csvfile) + for row in reader: + # Set default values if columns are missing + raw_for = str(row.get('for', '')).strip() + # Remove enclosing quotes + clean_for = raw_for.strip().strip('"').strip("'") + entry = { + 'for': clean_for, + 'add': row.get('add', '').strip(), + 'isRegex': row.get('isRegex', 'false').lower() == 'true' # Default: False + } + logging.debug("CSV row read: %s", row) + self.unified_cache[descriptor_key].append(entry) + csv_count += 1 + + logging.info("Loaded %d entries from CSV: %s for %s", csv_count, csv_path, descriptor_key) + + except Exception as e: + logging.error("Error loading CSV file %s: %s", csv_path, str(e)) + + def _find_description(self, descriptor_key, scan_value, bw_packet): + r"""!Find matching description for a scan value with Regex group support. + + The search is performed in two passes for performance optimization: + 1. First pass: Check for exact string matches (fast, no regex compilation) + 2. Second pass: Check regex patterns only if no exact match was found + + Regex patterns support capture groups that can be referenced in the description + using standard regex backreferences (\1, \2, etc.) via match.expand(). + + Example: + Pattern: r"(\d{7})" + Input: "1234567" + Description template: "RIC: \1" + Result: "RIC: 1234567" + + @param descriptor_key: Cache key identifying the descriptor configuration + @param scan_value: Value to search for in the descriptor cache + @param bw_packet: BOSWatch packet for wildcard replacement + @return: Matched description string or None if no match found + """ + + descriptions = self.unified_cache.get(descriptor_key, []) + scan_value_str = str(scan_value).strip() + + # First pass: Search for exact matches (performance optimization) + # Exact matches are checked first because they don't require regex compilation + for desc in descriptions: + if not desc.get('isRegex', False): + if desc['for'] == scan_value_str: + description_text = desc.get('add', '') + final_description = self._replace_wildcards(description_text, bw_packet) + return final_description + + # Second pass: Search for regex matches + # Only executed if no exact match was found in the first pass + for desc in descriptions: + if desc.get('isRegex', False): + match_pattern = desc.get('for', '') + try: + match = re.search(match_pattern, scan_value_str) + if match: + description_text = desc.get('add', '') + # match.expand() replaces backreferences (\1, \2, etc.) with captured groups + # Example: pattern="(\d+)-(\d+)", input="123-456", template="First: \1, Second: \2" + # result="First: 123, Second: 456" + expanded_description = match.expand(description_text) + final_description = self._replace_wildcards(expanded_description, bw_packet) + return final_description + except re.error as e: + logging.error("Invalid regex pattern '%s': %s", match_pattern, e) + + return None + + def _replace_wildcards(self, text, bw_packet): + r"""!Replace all available wildcards in description text dynamically.""" + if not text or '{' not in text: + return text + + result = text + + # Search for wildcards in the format {KEY} and replace them with values from the bw_packet + found_wildcards = re.findall(r"\{([A-Z0-9_]+)\}", result) + + for key in found_wildcards: + key_lower = key.lower() + value = bw_packet.get(key_lower) + + if value is not None: + result = result.replace(f"{{{key}}}", str(value)) + logging.debug("Replaced wildcard {%s} with value '%s'", key, value) + + return result def doWork(self, bwPacket): r"""!start an run of the module. @param bwPacket: A BOSWatch packet instance""" - for descriptor in self.config: - if not bwPacket.get(descriptor.get("scanField")): - break # scanField is not available in this packet - bwPacket.set(descriptor.get("descrField"), bwPacket.get(descriptor.get("scanField"))) - for description in descriptor.get("descriptions"): - if str(description.get("for")) == bwPacket.get(descriptor.get("scanField")): - logging.debug("Description '%s' added in packet field '%s'", - description.get("add"), descriptor.get("descrField")) - bwPacket.set(descriptor.get("descrField"), description.get("add")) - break # this descriptor has found a description - run next descriptor + logging.debug("Processing packet with mode: %s", bwPacket.get("mode")) + + # Process each descriptor configuration + for descriptor_config in self.config: + scan_field = descriptor_config.get("scanField") + descr_field = descriptor_config.get("descrField") + descriptor_key = f"{scan_field}_{descr_field}" + + logging.debug("Processing descriptor: scanField='%s', descrField='%s'", scan_field, descr_field) + + # Check if scanField is present in packet + scan_value = bwPacket.get(scan_field) + if scan_value is None: + logging.debug("scanField '%s' not found in packet, skipping", scan_field) + continue # scanField not available in this packet - try next descriptor + + # Set default value (content of scanField) + bwPacket.set(descr_field, str(scan_value)) + logging.debug("Set default value '%s' for field '%s'", scan_value, descr_field) + + # Search for matching description in unified cache + description = self._find_description(descriptor_key, scan_value, bwPacket) + + if description: + bwPacket.set(descr_field, description) + logging.info("Description set: '%s' -> '%s'", scan_value, description) + else: + logging.debug("No description found for value '%s' in field '%s'", scan_value, scan_field) + + logging.debug("Returning modified packet") return bwPacket def onUnload(self):