import configparser import sys from pathlib import Path from jinja2 import Environment, FileSystemLoader from .MsgData import MsgData # \brief Generates and maintains the message handling INI from loaded message data. # \details One section per message keyed by hex msgId (e.g. [0x0100]), carrying: # msg_id - the MSG_ID_* reference name, regenerated from the conf on every run. # action - send_always | send_delta | drop; defaults to drop, preserved across runs. # topic - MQTT topic; empty by default, preserved across runs. # Call loadConf() to load message definitions, then loadIni() to merge in existing # action/topic values, then write_ini() to write the result. class MsgHandlingIni(MsgData): DEFAULT_ACTION = 'drop' VALID_ACTIONS = ('send_always', 'send_delta', 'drop') # \brief Initializer def __init__(self): super().__init__() # \brief Load a .conf file and cache the data, enriching each entry with default INI fields. # \param[in] filename Filename of the .conf to load. # \param[in] clear If true, clear any previously loaded data before processing. # \return none def loadConf(self, filename, clear=True): super().loadConf(filename, clear) for msg in self.data.values(): msg['action'] = self.DEFAULT_ACTION msg['topic'] = '' # \brief Merge action/topic values from an existing INI into the loaded message data. # \details Only action and topic are read; msg_id is always regenerated from the conf. # Sections in the INI that are no longer in the conf are ignored (and warned if # they carried non-default values). Unknown action values are reset to drop. # \param[in] filename Path to the existing INI file; no-op if the file does not exist. # \return none def loadIni(self, filename): path = Path(filename) if not path.is_file(): return cfg = configparser.ConfigParser(inline_comment_prefixes=(';',)) cfg.read(path, encoding='utf-8') # just use self.data.keys() conf_keys = set(MsgData.value_to_hex_string(v) for v in self.data.keys()) for section in cfg.sections(): try: msg_id_value = int(section, 16) except ValueError: print(f"WARNING: could not convert section message ID {section} to valid message ID, skipping") continue if msg_id_value not in self.data.keys(): action = cfg.get(section, 'action', fallback=self.DEFAULT_ACTION).strip() topic = cfg.get(section, 'topic', fallback='').strip() if action != self.DEFAULT_ACTION or topic: print(f"WARNING: {MsgData.value_to_hex_string(msg_id_value)} no longer in Unhandled.conf, " f"dropping section with action={action} topic={topic or ''}") continue action = cfg.get(section, 'action', fallback=self.DEFAULT_ACTION).strip() if action not in self.VALID_ACTIONS: print(f"WARNING: {MsgData.value_to_hex_string(msg_id_value)} has invalid action \"{action}\", " f"resetting to {self.DEFAULT_ACTION}") action = self.DEFAULT_ACTION self.data[msg_id_value]['action'] = action self.data[msg_id_value]['topic'] = cfg.get(section, 'topic', fallback='').strip() # \brief Write the loaded message data to the INI file. # \param[in] filename Path to the INI file to create or update. # \return none def write_ini(self, filename): env = Environment(loader=FileSystemLoader(f'{Path(__file__).parent.absolute()}/templates'), keep_trailing_newline=True) template = env.get_template('MsgHandlingIni.jinja') render = template.render({'msg_ini': self}) with open(filename, mode='w', encoding='utf-8', newline='\n') as out_file: out_file.write(render) print(f"Wrote message INI {filename} with {len(self.data)} messages")