From 67e023bb28182114a0c284c94f7724d18fe5ba9c Mon Sep 17 00:00:00 2001 From: Ircama Date: Mon, 29 Jul 2024 03:29:30 +0200 Subject: [PATCH] Consolidated GUI --- README.md | 81 +++++++++++---- epson_print_conf.py | 50 +++++++++- gui.py | 11 +++ parse_devices.py | 234 ++++++++++++++++++++++++++++++-------------- printer_conf.pickle | Bin 0 -> 120319 bytes ui.py | 97 ++++++++++++++++-- 6 files changed, 367 insertions(+), 106 deletions(-) create mode 100644 gui.py create mode 100644 printer_conf.pickle diff --git a/README.md b/README.md index 99598e0..8e452b1 100644 --- a/README.md +++ b/README.md @@ -55,35 +55,64 @@ It is tested with Ubuntu / Windows Subsystem for Linux, Windows. ## Creating an executable for the GUI -Install pyinstaller if not already installed with `pip install pyinstaller`. +Install *pyinstaller* with `pip install pyinstaller`. -Run: `pyinstaller --onefile --noconsole ui.py`. +To create an executable file named *epson_print_conf.exe* from *ui.py*, run the following: -Run the exe file created in the *dist/* folder. +```bash +pyinstaller --onefile ui.py --name epson_print_conf --hidden-import babel.numbers --windowed +``` + +Then run the *epson_print_conf.exe* file created in the *dist/* folder, which has the same options of `ui.py`. + +The package includes another file named *gui.py*, which also automatically loads the configuration file *printer_conf.pickle*, merging it with the program configuration. In this case, the *epson_print_conf.spec* file helps creating an executable with *pyinstaller*. + +Run the following to build the executable: + +```bash +pip install pyinstaller # if not yet installed +curl -o devices.xml https://codeberg.org/attachments/147f41a3-a6ea-45f6-8c2a-25bac4495a1d +python3 parse_devices.py -a 192.168.178.29 -s XP-205 -p printer_conf.pickle # use your default IP address and printer model as default settings for the GUI +pyinstaller epson_print_conf.spec +``` + +Run the *epson_print_conf.exe* file created in the *dist/* folder. This executable program does not have options, embeds the *printer_conf.pickle* file and starts with the default IP address and printer model defined in the build phase. ## Usage -Running the GUI: +### Running the GUI with Python ``` python ui.py ``` -Using the command-line tool: +GUI usage: ``` -usage: epson_print_conf.py [-h] -m MODEL -a HOSTNAME [-p PORT] [-i] [-q QUERY_NAME] [--reset_waste_ink] [-d] - [--write-first-ti-received-time YEAR MONTH DAY] [--write-poweroff-timer MINUTES] - [--dry-run] [-R ADDRESS_SET] [-W ADDRESS_VALUE_SET] - [-e FIRST_ADDRESS LAST_ADDRESS] [--detect-key] [-S SEQUENCE_STRING] [-t TIMEOUT] - [-r RETRIES] [-c CONFIG_FILE] [--simdata SIMDATA_FILE] +ui.py [-h] [-P PICKLE_FILE] [-O] + +optional arguments: + -h, --help show this help message and exit + -P PICKLE_FILE, --pickle PICKLE_FILE + Save a pickle archive for subsequent load by ui.py and epson_print_conf.py + -O, --override Override the default configuration with the one of the pickle file instead of merging + +epson_print_conf GUI +``` + +### Using the command-line tool + +``` +epson_print_conf.py [-h] -m MODEL -a HOSTNAME [-p PORT] [-i] [-q QUERY_NAME] [--reset_waste_ink] [-d] [--write-first-ti-received-time YEAR MONTH DAY] + [--write-poweroff-timer MINUTES] [--dry-run] [-R ADDRESS_SET] [-W ADDRESS_VALUE_SET] [-e FIRST_ADDRESS LAST_ADDRESS] [--detect-key] + [-S SEQUENCE_STRING] [-t TIMEOUT] [-r RETRIES] [-c CONFIG_FILE] [--simdata SIMDATA_FILE] [-P PICKLE_FILE] [-O] optional arguments: -h, --help show this help message and exit -m MODEL, --model MODEL Printer model. Example: -m XP-205 (use ? to print all supported models) -a HOSTNAME, --address HOSTNAME - Printer host name or IP address. (Example: -m 192.168.1.87) + Printer host name or IP address. (Example: -a 192.168.1.87) -p PORT, --port PORT Printer port (default is 161) -i, --info Print all available information and statistics (default option) -q QUERY_NAME, --query QUERY_NAME @@ -98,8 +127,7 @@ optional arguments: -R ADDRESS_SET, --read-eeprom ADDRESS_SET Read the values of a list of printer EEPROM addreses. Format is: address [, ...] -W ADDRESS_VALUE_SET, --write-eeprom ADDRESS_VALUE_SET - Write related values to a list of printer EEPROM addresses. Format is: address: value - [, ...] + Write related values to a list of printer EEPROM addresses. Format is: address: value [, ...] -e FIRST_ADDRESS LAST_ADDRESS, --eeprom-dump FIRST_ADDRESS LAST_ADDRESS Dump EEPROM --detect-key Detect the read_key via brute force @@ -110,10 +138,12 @@ optional arguments: -r RETRIES, --retries RETRIES SNMP GET retries (floating point argument) -c CONFIG_FILE, --config CONFIG_FILE - read a configuration file including the full log dump of a previous operation with - '-d' flag (instead of accessing the printer via SNMP) + read a configuration file including the full log dump of a previous operation with '-d' flag (instead of accessing the printer via SNMP) --simdata SIMDATA_FILE write SNMP dictionary map to simdata file + -P PICKLE_FILE, --pickle PICKLE_FILE + Load a pickle configuration archive + -O, --override Override the default configuration with the one of the pickle file instead of merging Epson Printer Configuration via SNMP (TCP/IP) ``` @@ -160,7 +190,7 @@ Note: resetting the ink waste counter is just removing a warning; not replacing Within an [issue](https://codeberg.org/atufi/reinkpy/issues/12#issue-716809) in repo https://codeberg.org/atufi/reinkpy there is an interesting [attachment](https://codeberg.org/attachments/147f41a3-a6ea-45f6-8c2a-25bac4495a1d) which reports an extensive XML database of Epson model features. -The program "parse_devices.py" transforms this XML DB into the dictionary that *epson_print_conf.py* can use. +The program *parse_devices.py* transforms this XML DB into the dictionary that *epson_print_conf.py* can use. Here is a simple procedure to download that DB and run *parse_devices.py* to search for the XP-205 model and produce the related PRINTER_CONFIG dictionary to the standard output: @@ -169,14 +199,15 @@ curl -o devices.xml https://codeberg.org/attachments/147f41a3-a6ea-45f6-8c2a-25b python3 parse_devices.py -i -m XP-205 ``` -After generating the related printer configuration, *epson_print_conf.py* shall be manually edited to copy/paste the output of *parse_devices.py* within its PRINTER_CONFIG dictionary. +After generating the related printer configuration, *epson_print_conf.py* shall be manually edited to copy/paste the output of *parse_devices.py* within its PRINTER_CONFIG dictionary. Alternatively, the program is able to create a *pickle* configuration file, which the other programs can load. The `-m` option is optional and is used to filter the printer model in scope. If the produced output is not referred to the target model, use part of the model name as a filter (e.g., only the digits, like `parse_devices.py -i -m 315`) and select the appropriate model from the output. Program usage: ``` -parse_devices.py [-h] [-m PRINTER_MODEL] [-l LINE_LENGTH] [-i] [-d] [-t] [-v] [-f] [-e] [-c CONFIG_FILE] +parse_devices.py [-h] [-m PRINTER_MODEL] [-l LINE_LENGTH] [-i] [-d] [-t] [-v] [-f] [-e] [-c CONFIG_FILE] [-s DEFAULT_MODEL] -a HOSTNAME [-p PICKLE_FILE] [-I] + [-N] [-A] [-S] optional arguments: -h, --help show this help message and exit @@ -192,6 +223,16 @@ optional arguments: -e, --errors Add last_printer_fatal_errors -c CONFIG_FILE, --config CONFIG_FILE use the XML configuration file to generate the configuration + -s DEFAULT_MODEL, --default_model DEFAULT_MODEL + Default printer model. Example: -s XP-205 + -a HOSTNAME, --address HOSTNAME + Default printer host name or IP address. (Example: -a 192.168.1.87) + -p PICKLE_FILE, --pickle PICKLE_FILE + Save a pickle archive for subsequent load by ui.py and epson_print_conf.py + -I, --keep_invalid Do not remove printers without write_key or without read_key + -N, --keep_names Do not replace original names with converted names and add printers for all optional names + -A, --no_alias Do not add aliases for same printer with different names and remove aliased printers + -S, --no_same_as Do not add "same-as" for similar printers with different names Generate printer configuration from devices.xml ``` @@ -316,9 +357,11 @@ AC = Value ### Specification ```python -EpsonPrinter(model, hostname, port, timeout, retries, dry_run) +EpsonPrinter(conf_dict, replace_conf, model, hostname, port, timeout, retries, dry_run) ``` +- `conf_dict`: optional configuration file in place of the default PRINTER_CONFIG (optional, default to `{}`) +- `replace_conf`: (optional, default to False) set to True to replace PRINTER_CONFIG with `conf_dict` instead of merging it - `model`: printer model - `hostname`: IP address or network name of the printer - `port`: SNMP port number (default is 161) diff --git a/epson_print_conf.py b/epson_print_conf.py index 924d7cb..9367a5a 100644 --- a/epson_print_conf.py +++ b/epson_print_conf.py @@ -643,6 +643,8 @@ class EpsonPrinter: def __init__( self, + conf_dict: dict = {}, + replace_conf = False, model: str = None, hostname: str = None, port: int = 161, @@ -651,7 +653,19 @@ class EpsonPrinter: dry_run: bool = False ) -> None: """Initialise printer model.""" + def merge(source, destination): + for key, value in source.items(): + if isinstance(value, dict): + merge(value, destination.setdefault(key, {})) + else: + if key == "alias" and "alias" in destination: + destination[key] += value + else: + destination[key] = value + return destination # process "alias" definintion + if conf_dict and replace_conf: + self.PRINTER_CONFIG = conf_dict for printer_name, printer_data in self.PRINTER_CONFIG.copy().items(): if "alias" in printer_data: aliases = printer_data["alias"] @@ -672,6 +686,16 @@ class EpsonPrinter: ) else: self.PRINTER_CONFIG[alias_name] = printer_data + if conf_dict and not replace_conf: + self.PRINTER_CONFIG = merge(self.PRINTER_CONFIG, conf_dict) + for key, values in self.PRINTER_CONFIG.items(): + if 'alias' in values: + values['alias'] = [ + i for i in values['alias'] + if i not in self.PRINTER_CONFIG + ] + if not values['alias']: + del values['alias'] # process "same-as" definintion for printer_name, printer_data in self.PRINTER_CONFIG.copy().items(): if "same-as" in printer_data: @@ -2035,7 +2059,7 @@ if __name__ == "__main__": '--address', dest='hostname', action="store", - help='Printer host name or IP address. (Example: -m 192.168.1.87)', + help='Printer host name or IP address. (Example: -a 192.168.1.87)', required=True) parser.add_argument( '-p', @@ -2174,6 +2198,24 @@ if __name__ == "__main__": nargs=1, metavar='SIMDATA_FILE' ) + parser.add_argument( + '-P', + "--pickle", + dest='pickle', + type=argparse.FileType('rb'), + help="Load a pickle configuration archive", + default=None, + nargs=1, + metavar='PICKLE_FILE' + ) + parser.add_argument( + '-O', + "--override", + dest='override', + action='store_true', + help="Override the default configuration with the one of the pickle " + "file instead of merging", + ) args = parser.parse_args() logging_level = logging.WARNING @@ -2198,7 +2240,13 @@ if __name__ == "__main__": if args.debug: logging.getLogger().setLevel(logging.DEBUG) + conf_dict = {} + if args.pickle: + conf_dict = pickle.load(args.pickle[0]) + printer = EpsonPrinter( + conf_dict=conf_dict, + replace_conf=args.override, model=args.model, hostname=args.hostname, port=args.port, diff --git a/gui.py b/gui.py new file mode 100644 index 0000000..1e78e04 --- /dev/null +++ b/gui.py @@ -0,0 +1,11 @@ +import pickle +from ui import EpsonPrinterUI +from os import path + +PICKLE_CONF_FILE = "printer_conf.pickle" + +path_to_pickle = path.abspath(path.join(path.dirname(__file__), PICKLE_CONF_FILE)) +with open(path_to_pickle, 'rb') as fp: + conf_dict = pickle.load(fp) +app = EpsonPrinterUI(conf_dict=conf_dict, replace_conf=False) +app.mainloop() diff --git a/parse_devices.py b/parse_devices.py index 8638aa0..586e08a 100644 --- a/parse_devices.py +++ b/parse_devices.py @@ -219,71 +219,81 @@ def generate_config(config, traverse, add_fatal_errors, full, printer_model): printer_config[printer_short_name] = chars return printer_config -def normalize_config(config): +def normalize_config( + config, + remove_invalid, + expand_names, + add_alias, + add_same_as, + ): # Remove printers without write_key or without read_key - for base_key, base_items in config.copy().items(): - if 'write_key' not in base_items: - del config[base_key] - continue - if 'read_key' not in base_items: - del config[base_key] - continue + if remove_invalid: + for base_key, base_items in config.copy().items(): + if 'write_key' not in base_items: + del config[base_key] + continue + if 'read_key' not in base_items: + del config[base_key] + continue # Replace original names with converted names and add printers for all optional names - for key, items in config.copy().items(): - printer_list = get_printer_models(key) - del config[key] - for i in printer_list: - if i in config and config[i] != items: - print("ERROR key", key) - quit() - config[i] = items + if expand_names: + for key, items in config.copy().items(): + printer_list = get_printer_models(key) + del config[key] + for i in printer_list: + if i in config and config[i] != items: + print("ERROR key", key) + quit() + config[i] = items # Add aliases for same printer with different names and remove aliased printers - for base_key, base_items in config.copy().items(): - found = False - for key, items in config.copy().items(): - if not found: - if base_key == key and base_key in config: - found = True - continue - if base_key != key and items == base_items: # different name, same printer - if "alias" not in config[base_key]: - config[base_key]["alias"] = [] - for i in get_printer_models(key): - if i not in config[base_key]["alias"]: - config[base_key]["alias"].append(i) - del config[key] + if add_alias: + for base_key, base_items in config.copy().items(): + found = False + for key, items in config.copy().items(): + if not found: + if base_key == key and base_key in config: + found = True + continue + if base_key != key and items == base_items: # different name, same printer + if "alias" not in config[base_key]: + config[base_key]["alias"] = [] + for i in get_printer_models(key): + if i not in config[base_key]["alias"]: + config[base_key]["alias"].append(i) + del config[key] # Add "same-as" for almost same printer (IGNORED_KEYS) with different names - IGNORED_KEYS = ['write_key', 'read_key', 'alias', 'main_waste', 'borderless_waste'] - for base_key, base_items in config.copy().items(): - found = False - for key, items in config.copy().items(): - if not found: - if base_key == key and base_key in config: - found = True - continue - if base_key != key: - if equal_dicts(base_items, items, IGNORED_KEYS): # everything but the IGNORED_KEYS is the same - # Get the IGNORED_KEYS from the printer - write_key = base_items['write_key'] - read_key = base_items['read_key'] - alias = base_items['alias'] if 'alias' in base_items else [] - main_waste = base_items['main_waste'] if 'main_waste' in base_items else [] - borderless_waste = base_items['borderless_waste'] if 'borderless_waste' in base_items else [] - # Rebuild the printer with only the IGNORED_KEYS, then add the 'same-as' - del config[base_key] - config[base_key] = {} - config[base_key]['write_key'] = write_key - config[base_key]['read_key'] = read_key - if alias: - config[base_key]['alias'] = alias - if main_waste: - config[base_key]['main_waste'] = main_waste - if borderless_waste: - config[base_key]['borderless_waste'] = borderless_waste - config[base_key]['same-as'] = key + if add_same_as: + IGNORED_KEYS = ['write_key', 'read_key', 'alias', 'main_waste', 'borderless_waste'] + for base_key, base_items in config.copy().items(): + found = False + for key, items in config.copy().items(): + if not found: + if base_key == key and base_key in config: + found = True + continue + if base_key != key: + if equal_dicts(base_items, items, IGNORED_KEYS): # everything but the IGNORED_KEYS is the same + # Get the IGNORED_KEYS from the printer + write_key = base_items['write_key'] + read_key = base_items['read_key'] + alias = base_items['alias'] if 'alias' in base_items else [] + main_waste = base_items['main_waste'] if 'main_waste' in base_items else [] + borderless_waste = base_items['borderless_waste'] if 'borderless_waste' in base_items else [] + # Rebuild the printer with only the IGNORED_KEYS, then add the 'same-as' + del config[base_key] + config[base_key] = {} + config[base_key]['write_key'] = write_key + config[base_key]['read_key'] = read_key + if alias: + config[base_key]['alias'] = alias + if main_waste: + config[base_key]['main_waste'] = main_waste + if borderless_waste: + config[base_key]['borderless_waste'] = borderless_waste + config[base_key]['same-as'] = key return config @@ -294,69 +304,69 @@ def equal_dicts(a, b, ignore_keys): if __name__ == "__main__": import argparse + import pickle parser = argparse.ArgumentParser( epilog='Generate printer configuration from devices.xml' ) - parser.add_argument( '-m', '--model', dest='printer_model', default=False, action="store", - help='Printer model. Example: -m XP-205') - + help='Printer model. Example: -m XP-205' + ) parser.add_argument( '-l', '--line', dest='line_length', type=int, help='Set line length of the output (default: 120)', - default=120) - + default=120 + ) parser.add_argument( '-i', '--indent', dest='indent', action='store_true', - help='Indent output of 4 spaces') - + help='Indent output of 4 spaces' + ) parser.add_argument( '-d', '--debug', dest='debug', action='store_true', - help='Print debug information') - + help='Print debug information' + ) parser.add_argument( '-t', '--traverse', dest='traverse', action='store_true', - help='Traverse the XML, dumping content related to the printer model') - + help='Traverse the XML, dumping content related to the printer model' + ) parser.add_argument( '-v', '--verbose', dest='verbose', action='store_true', - help='Print verbose information') - + help='Print verbose information' + ) parser.add_argument( '-f', '--full', dest='full', action='store_true', - help='Generate additional tags') - + help='Generate additional tags' + ) parser.add_argument( '-e', '--errors', dest='add_fatal_errors', action='store_true', - help='Add last_printer_fatal_errors') - + help='Add last_printer_fatal_errors' + ) parser.add_argument( '-c', "--config", @@ -367,6 +377,59 @@ if __name__ == "__main__": nargs=1, metavar='CONFIG_FILE' ) + parser.add_argument( + '-s', + '--default_model', + dest='default_model', + action="store", + help='Default printer model. Example: -s XP-205' + ) + parser.add_argument( + '-a', + '--address', + dest='hostname', + action="store", + help='Default printer host name or IP address. (Example: -a 192.168.1.87)', + required=True + ) + parser.add_argument( + '-p', + "--pickle", + dest='pickle', + type=argparse.FileType('wb'), + help="Save a pickle archive for subsequent load by ui.py and epson_print_conf.py", + default=0, + nargs=1, + metavar='PICKLE_FILE' + ) + parser.add_argument( + '-I', + '--keep_invalid', + dest='keep_invalid', + action='store_true', + help='Do not remove printers without write_key or without read_key' + ) + parser.add_argument( + '-N', + '--keep_names', + dest='keep_names', + action='store_true', + help='Do not replace original names with converted names and add printers for all optional names' + ) + parser.add_argument( + '-A', + '--no_alias', + dest='no_alias', + action='store_true', + help='Do not add aliases for same printer with different names and remove aliased printers' + ) + parser.add_argument( + '-S', + '--no_same_as', + dest='no_same_as', + action='store_true', + help='Do not add "same-as" for similar printers with different names' + ) args = parser.parse_args() if args.debug: @@ -390,7 +453,26 @@ if __name__ == "__main__": full=args.full, printer_model=args.printer_model ) - normalized_config = normalize_config(printer_config) + normalized_config = normalize_config( + config=printer_config, + remove_invalid=not args.keep_invalid, + expand_names=not args.keep_names, + add_alias=not args.no_alias, + add_same_as=not args.no_same_as, + ) + if args.default_model: + if "internal_data" not in normalized_config: + normalized_config["internal_data"] = {} + normalized_config["internal_data"]["default_model"] = args.default_model + if args.hostname: + if "internal_data" not in normalized_config: + normalized_config["internal_data"] = {} + normalized_config["internal_data"]["hostname"] = args.hostname + if args.pickle: + pickle.dump(normalized_config, args.pickle[0]) # serialize the list + args.pickle[0].close() + quit() + try: import black config_str = "PRINTER_CONFIG = " + repr(normalized_config) diff --git a/printer_conf.pickle b/printer_conf.pickle new file mode 100644 index 0000000000000000000000000000000000000000..e89841f23e1aac1bb05725d6ab3c33b44c9df3ce GIT binary patch literal 120319 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 T)Ec;#*Cg|WHSl&;Dr^3K13vj| literal 0 HcmV?d00001 diff --git a/ui.py b/ui.py index facd029..b4cbaf2 100644 --- a/ui.py +++ b/ui.py @@ -125,7 +125,7 @@ class ToolTip: class EpsonPrinterUI(tk.Tk): - def __init__(self): + def __init__(self, conf_dict={}, replace_conf=False): super().__init__() self.title("Epson Printer Configuration - v" + VERSION) self.geometry("450x500") @@ -133,6 +133,8 @@ class EpsonPrinterUI(tk.Tk): self.printer_scanner = PrinterScanner() self.ip_list = [] self.ip_list_cycle = None + self.conf_dict = conf_dict + self.replace_conf = replace_conf # configure the main window to be resizable self.columnconfigure(0, weight=1) @@ -167,13 +169,21 @@ class EpsonPrinterUI(tk.Tk): model_frame.columnconfigure(1, weight=1) self.model_var = tk.StringVar() + if ( + "internal_data" in conf_dict + and "default_model" in conf_dict["internal_data"] + ): + self.model_var.set(conf_dict["internal_data"]["default_model"]) ttk.Label(model_frame, text="Model:").grid( row=0, column=0, sticky=tk.W, padx=PADX ) self.model_dropdown = ttk.Combobox( model_frame, textvariable=self.model_var, state="readonly" ) - self.model_dropdown["values"] = sorted(EpsonPrinter().valid_printers) + self.model_dropdown["values"] = sorted(EpsonPrinter( + conf_dict=self.conf_dict, + replace_conf=self.replace_conf + ).valid_printers) self.model_dropdown.grid( row=0, column=1, pady=PADY, padx=PADX, sticky=(tk.W, tk.E) ) @@ -193,6 +203,11 @@ class EpsonPrinterUI(tk.Tk): ip_frame.columnconfigure(1, weight=1) self.ip_var = tk.StringVar() + if ( + "internal_data" in conf_dict + and "hostname" in conf_dict["internal_data"] + ): + self.ip_var.set(conf_dict["internal_data"]["hostname"]) ttk.Label(ip_frame, text="IP Address:").grid( row=0, column=0, sticky=tk.W, padx=PADX ) @@ -446,7 +461,12 @@ class EpsonPrinterUI(tk.Tk): self.config(cursor="") self.update() return - printer = EpsonPrinter(model=model, hostname=ip_address) + printer = EpsonPrinter( + conf_dict=self.conf_dict, + replace_conf=self.replace_conf, + model=model, + hostname=ip_address + ) try: po_timer = printer.stats()["stats"]["Power off timer"] self.status_text.insert( @@ -478,7 +498,12 @@ class EpsonPrinterUI(tk.Tk): self.config(cursor="") self.update_idletasks() return - printer = EpsonPrinter(model=model, hostname=ip_address) + printer = EpsonPrinter( + conf_dict=self.conf_dict, + replace_conf=self.replace_conf, + model=model, + hostname=ip_address + ) try: po_timer = printer.stats()["stats"]["Power off timer"] po_timer = self.po_timer_var.get() @@ -525,7 +550,12 @@ class EpsonPrinterUI(tk.Tk): self.config(cursor="") self.update_idletasks() return - printer = EpsonPrinter(model=model, hostname=ip_address) + printer = EpsonPrinter( + conf_dict=self.conf_dict, + replace_conf=self.replace_conf, + model=model, + hostname=ip_address + ) try: date_string = datetime.strptime( printer.stats()["stats"]["First TI received time"], "%d %b %Y" @@ -560,7 +590,12 @@ class EpsonPrinterUI(tk.Tk): self.config(cursor="") self.update_idletasks() return - printer = EpsonPrinter(model=model, hostname=ip_address) + printer = EpsonPrinter( + conf_dict=self.conf_dict, + replace_conf=self.replace_conf, + model=model, + hostname=ip_address + ) try: date_string = datetime.strptime( printer.stats()["stats"]["First TI received time"], "%d %b %Y" @@ -624,7 +659,12 @@ class EpsonPrinterUI(tk.Tk): self.config(cursor="") self.update_idletasks() return - printer = EpsonPrinter(model=model, hostname=ip_address) + printer = EpsonPrinter( + conf_dict=self.conf_dict, + replace_conf=self.replace_conf, + model=model, + hostname=ip_address + ) try: self.show_treeview() @@ -662,7 +702,12 @@ class EpsonPrinterUI(tk.Tk): self.config(cursor="") self.update_idletasks() return - printer = EpsonPrinter(model=model, hostname=ip_address) + printer = EpsonPrinter( + conf_dict=self.conf_dict, + replace_conf=self.replace_conf, + model=model, + hostname=ip_address + ) try: printer.stats() # query the printer first response = messagebox.askyesno( @@ -714,7 +759,10 @@ class EpsonPrinterUI(tk.Tk): ) self.ip_var.set(printers[0]["ip"]) for model in get_printer_models(printers[0]["name"]): - if model in EpsonPrinter().valid_printers: + if model in EpsonPrinter( + conf_dict=self.conf_dict, + replace_conf=self.replace_conf + ).valid_printers: self.model_var.set(model) break else: @@ -835,5 +883,34 @@ class EpsonPrinterUI(tk.Tk): if __name__ == "__main__": - app = EpsonPrinterUI() + import argparse + import pickle + + parser = argparse.ArgumentParser( + epilog='epson_print_conf GUI' + ) + parser.add_argument( + '-P', + "--pickle", + dest='pickle', + type=argparse.FileType('rb'), + help="Save a pickle archive for subsequent load by ui.py and epson_print_conf.py", + default=None, + nargs=1, + metavar='PICKLE_FILE' + ) + parser.add_argument( + '-O', + "--override", + dest='override', + action='store_true', + help="Override the default configuration with the one of the pickle " + "file instead of merging", + ) + args = parser.parse_args() + conf_dict = {} + if args.pickle: + conf_dict = pickle.load(args.pickle[0]) + + app = EpsonPrinterUI(conf_dict=conf_dict, replace_conf=args.override) app.mainloop()