diff --git a/pnp/README.md b/pnp/README.md
index 7ab7f3d51861c2aa770f4fab64f343f1749ee875..cd27c65989c5d1c54c8c617b1deb8f6967916ab0 100644
--- a/pnp/README.md
+++ b/pnp/README.md
@@ -120,16 +120,20 @@ to use the PnP server you need to configure the server by modifying the followin
 #### Global settings [**open-pnp.toml**](/pnp/open-pnp.toml)
 
 ```
+# [settings]
 # bind_pnp_server = "0.0.0.0"
+# bind_pnp_server = "::"
 # port = 8080
-# time_format = "%Y-%m-%dT%H:%M:%S"
-# status_refresh = 60
+# time_format = "%y-%m-%dt%h:%m:%s"
+# status_refresh = 10
 # debug = false
-# log_to_console = false
+# log_to_file = true
 # log_file = "log/pnp_debug.log"
+# default_cfg_file = "default.cfg"
 # image_data = "images.toml"
-# image_base_url = "http://192.168.10.133:8080/images"
-# config_base_url = "http://192.168.10.133:8080/configs"
+# image_url = "http://192.168.10.133:8080/images"
+# config_url = "http://192.168.10.133:8080/configs"
+# default_cfg = "DEFAULT.cfg"
 ```
 
 - **bind_pnp_server**: the IP-address of your open-pnp server box. (Use `"::"` for IPv6)
@@ -142,6 +146,7 @@ to use the PnP server you need to configure the server by modifying the followin
 - **image_data**: the file containing the data of your IOS/IOS-XE images
 - **image_base_url**: the base URL for your images 
 - **config_base_url**: the base URL for your configuration files
+- **default_cfg**: default config to use if no device specific config is found.
 
 **Note**: you need to uncomment (remove `# `) the lines if you change the values.
 
@@ -170,50 +175,46 @@ models = ["C1000-8T-2G-L", "C1000-24P-4G-L", "C1000-24T-4G-L", "C1000-24T-4X-L",
 ### Command Line Options
 
 With the Command Line Options you can override the default values, and the values from the _open-pnp.toml_ config file.
-With the option --config_file CONFIG_FILE you can specify a costume config file to use instead of _open.pnp.toml_.   
+With the option --config_file CONFIG_FILE you can specify a costume config file to use instead of _open-pnp.toml_.   
 
 ```
 $ ./open-pnp.py -h
-usage: open-pnp.py [-h] [--bind_pnp_server BIND_PNP_SERVER] [--port PORT]
-                   [--time_format TIME_FORMAT]
-                   [--status_refresh STATUS_REFRESH] [--debug]
-                   [--log_to_console] [--log_file LOG_FILE]
-                   [--image_data IMAGE_DATA] [--image_url IMAGE_URL]
-                   [--config_url CONFIG_URL] [--config_file CONFIG_FILE]
+usage: open-pnp.py [-h] [-b BIND_PNP_SERVER] [-p PORT] [-r STATUS_REFRESH]
+                   [-v] [--config_file CONFIG_FILE] [--config_url CONFIG_URL]
+                   [--image_data IMAGE_DATA] [--image_url IMAGE_URL] [--debug]
+                   [--default_cfg DEFAULT_CFG] [--log_file LOG_FILE]
+                   [--log_to_console] [--time_format TIME_FORMAT]
 
 This is a basic implementation of the Cisco PnP protocol. It is intended to
-roll out image updates and configurations for Cisco IOS/IOS-XE devices on
-day0.
+roll out image updates and configurations for Cisco IOS/IOS-XE devices on day0.
 
-optional arguments:
+20230223.v1.0.1 | Written by: thl-cmk, for more information see: https://thl-cmk.hopto.org
+
+options:
   -h, --help            show this help message and exit
-  --bind_pnp_server BIND_PNP_SERVER, -b BIND_PNP_SERVER
+  -b BIND_PNP_SERVER, --bind_pnp_server BIND_PNP_SERVER
                         Bind PnP server to IP-address. (default: 0.0.0.0)
-  --port PORT, -p PORT  TCP port to listen on. (default: 8080)
-  --time_format TIME_FORMAT
-                        Format string to render time. (default:
-                        %Y-%m-%dT%H:%M:%S)
-  --status_refresh STATUS_REFRESH, -r STATUS_REFRESH
-                        Time in seconds to refresh PnP server status page.
-                        (default: 60)
-  --debug               Enable Debug output send to "log_file".
-  --log_to_console      Enable debug output send to stdout (requires --debug).
-  --log_file LOG_FILE   Path/name of the logfile. (default: log/pnp_debug.log,
-                        requires --debug)
+  -p PORT, --port PORT  TCP port to listen on. (default: 8080)
+  -r STATUS_REFRESH, --status_refresh STATUS_REFRESH
+                        Time in seconds to refresh PnP server status page. (default: 60)
+  -v, --version         Print open-pnp-server version and exit
+  --config_file CONFIG_FILE
+                        Path/name of open PnP server config file. (default: open-pnp.toml)
+  --config_url CONFIG_URL
+                        Download URL for config files. I.e. http://192.168.10.133:8080/configs
   --image_data IMAGE_DATA
-                        File containing the image description. (default:
-                        images.toml)
+                        File containing the image description. (default: images.toml)
   --image_url IMAGE_URL
-                        Download URL for image files. I.e.
-                        http://192.168.10.133:8080/images
-  --config_url CONFIG_URL
-                        Download URL for config files. I.e.
-                        http://192.168.10.133:8080/configs
-  --config_file CONFIG_FILE
-                        Path/name of open PnP server config file. (default:
-                        open-pnp.toml)
+                        Download URL for image files. I.e. http://192.168.10.133:8080/images
+  --debug               Enable Debug output send to "log_file".
+  --default_cfg DEFAULT_CFG
+                        default config to use if no device specific config is found. (default: DEFAULT.cfg)
+  --log_file LOG_FILE   Path/name of the logfile. (default: log/pnp_debug.log, requires --debug)
+  --log_to_console      Enable debug output send to stdout (requires --debug).
+  --time_format TIME_FORMAT
+                        Format string to render time. (default: %Y-%m-%dT%H:%M:%S)
 
-Written by: thl-cmk, for more information see: https://thl-cmk.hopto.org
+Usage: python open-pnp.py --config_url  http://192.168.10.133:8080/configs --image_url http://192.168.10.133:8080/images
 
 ```
 
diff --git a/pnp/images.toml b/pnp/images.toml
index 4d301b43e0975e13b7631f15c0d4dc7571ede012..0c8a3945b6317f41f42a7a67d1bcb8224b011d17 100644
--- a/pnp/images.toml
+++ b/pnp/images.toml
@@ -1,15 +1,3 @@
-["cat9k_iosxe.17.06.01.SPA.bin"]
-version = "17.6.1"
-md5 = "fdb9c92bae37f9130d0ee6761afe2919"
-size = 1
-models = ["C9500-24Q", "C9300-24P"]
-
-["asr1000-universalk9.17.05.01a.SPA.bin"]
-version = "17.5.1a"
-md5 = "0e4b1fc1448f8ee289634a41f75dc215"
-size = 1
-models = ["ASR1001-HX"]
-
 ["c1000-universalk9-mz.152-7.E7.bin"]
 version = "15.2(7)E7"
 md5 = "1e6f508499c36434f7035b83a4018390"
@@ -40,12 +28,6 @@ md5 = "1c4c0597d355a0926c3d7198c1167cae"
 size = 22967296
 models = ["WS-C3560CX-12PD-S"]
 
-["ir1101-universalk9.17.06.01.SPA.bin"]
-version = "17.6.1"
-md5 = "2654cd5734a0722cfd3f521c25071552"
-size = 638837341
-models = []
-
 ["ir1101-universalk9.17.06.01a.SPA.bin"]
 version = "17.6.1a"
 md5 = "492f47e99136de9e0982c755ac1e1972"
diff --git a/pnp/open-pnp.py b/pnp/open-pnp.py
index 33a971250b307467d02d03aad2a1cf3326e2497b..83d9e4943c899e4c8954b197ec1469f219fec422 100755
--- a/pnp/open-pnp.py
+++ b/pnp/open-pnp.py
@@ -33,406 +33,50 @@
 # 2023-02-22: removed regex -> was not working with PID: ISR4451-X/K9
 #             added PNP_SERVER_VERSION
 # 2023-02-23: added cli option -v/--version, --default_cfg
-
+# 2023-02-26: reorganized open-pnp.py in to open_pnp_classes.py and open_pnp_utils.py
+#
 # pip install flask xmltodict requests ifaddr tomli
 #
 # ToDo:
 #       add remove inactive job on IOS-XE devices if no space for image update
 
 # system libs
-import logging
-from logging.handlers import RotatingFileHandler
 # from re import compile as re_compile
 from time import strftime
-from typing import Optional, List, Dict, Any
+from typing import Optional, Dict, Any
 from requests import head
-from sys import stdout
-from argparse import (
-    Namespace as arg_Namespace,
-    ArgumentParser,
-    RawTextHelpFormatter,
-)
+from logging import getLogger
 
 # additional libs
-from flask import Flask, request, send_from_directory, render_template, Response, redirect, cli
+from flask import (
+    Flask, 
+    Response,
+    send_from_directory, 
+    render_template, 
+    redirect, 
+    request,
+    cli
+)
 from xmltodict import parse as xml_parse
-from ifaddr import get_adapters
-from tomli import load as toml_load
-from tomli import TOMLDecodeError
-
-PNP_SERVER_VERSION = '20230223.v1.0.1'
-
-
-class Settings:
-    def __init__(
-            self,
-            cli_args: Dict[str, any],
-            version: bool = False,
-            cfg_file: Optional[str] = 'open-pnp.toml',
-            image_data: Optional[str] = 'images.toml',
-            bind_pnp_server: Optional[str] = '0.0.0.0',
-            port: Optional[int] = 8080,
-            time_format: Optional[str] = '%Y-%m-%dT%H:%M:%S',
-            status_refresh: Optional[int] = 60,
-            debug: Optional[bool] = False,
-            log_to_console: Optional[bool] = False,
-            log_file: Optional[str] = 'log/pnp_debug.log',
-            image_url: Optional[str] = '',
-            config_url: Optional[str] = '',
-            default_cfg: Optional[str] = 'DEFAULT.cfg',
-    ):
-        self.__settings = {
-            'cfg_file': cfg_file,
-            'version': version,
-            'image_data': image_data,
-            'bind_pnp_server': bind_pnp_server,
-            'port': port,
-            'time_format': time_format,
-            'status_refresh': status_refresh,
-            'debug': debug,
-            'log_to_console': log_to_console,
-            'log_file': log_file,
-            'image_url': image_url,
-            'config_url': config_url,
-            'default_cfg_file': default_cfg,
-        }
-        self.__args = {}
-        self.__set_cli_args(cli_args)
-
-    def __set_cli_args(self, cli_args: Dict[str, any]):
-        self.__args = ({k: v for k, v in cli_args.items() if v})
-        self.__settings.update(self.__args)
-
-    def update(self, cfg_file: str):
-        try:
-            with open(cfg_file, 'rb') as f:
-                self.__settings.update(toml_load(f))
-        except FileNotFoundError as e:
-            print(f'ERROR: Data file {cfg_file} not found! ({e})')
-            exit(1)
-        except TOMLDecodeError as e:
-            print(f'ERROR: Data file {cfg_file} is not in valid toml format! ({e})')
-            exit(2)
-
-        self.__settings.update(self.__args)
-
-    @property
-    def cfg_file(self) -> str:
-        return self.__settings['cfg_file']
-
-    @property
-    def version(self) -> bool:
-        return self.__settings['version']
-
-    @property
-    def image_data(self) -> str:
-        return self.__settings['image_data']
-
-    @property
-    def bind_pnp_server(self) -> str:
-        return self.__settings['bind_pnp_server']
-
-    @property
-    def port(self) -> int:
-        return self.__settings['port']
-
-    @property
-    def time_format(self) -> str:
-        return self.__settings['time_format']
-
-    @property
-    def status_refresh(self) -> int:
-        return self.__settings['status_refresh']
-
-    @property
-    def debug(self) -> bool:
-        return self.__settings['debug']
-
-    @property
-    def log_to_console(self) -> bool:
-        return self.__settings['log_to_console']
-
-    @property
-    def log_file(self) -> str:
-        return self.__settings['log_file']
-
-    @property
-    def image_url(self) -> str:
-        return self.__settings['image_url']
-
-    @property
-    def config_url(self) -> str:
-        return self.__settings['config_url']
-
-    @property
-    def default_cfg(self) -> str:
-        return self.__settings['default_cfg']
-
-
-class SoftwareImage:
-    def __init__(self, image: str, version: str, md5: str, size: int,):
-        self.image: str = image
-        self.version: str = version
-        self.md5: str = md5
-        self.size: int = size
-
-
-class ErrorCodes:
-    __readable = {
-        0: 'No error',
-        100: 'unknown platform',
-        101: 'no free space for update',
-        102: 'unknown image',
-        103: 'config file not found',
-        104: 'image file not found',
-
-        1412: 'Invalid input detected (config)',
-        1413: 'Invalid input detected',
-        1609: 'Error while retrieving device filesystem info',
-        1816: 'Error verifying checksum for Image',
-        1829: 'Image copy was unsuccessful',
-        1803: 'Source file not found',
-    }
-
-    def __init__(self):
-        self.ERROR_NO_ERROR = 0
-
-        self.ERROR_NO_PLATFORM = 100
-        self.ERROR_NO_FREE_SPACE = 101
-        self.ERROR_NO_IMAGE = 102
-        self.ERROR_NO_CFG_FILE = 103
-        self.ERROR_NO_IMAGE_FILE = 104
-
-        self.PNP_ERROR_INVALID_CONFIG = 1412
-        self.PNP_ERROR_INVALID_INPUT = 1413
-        self.PNP_ERROR_NO_FILESYSTEM_INFO = 1609
-        self.PNP_ERROR_BAD_CHECKSUM = 1816
-        self.PNP_ERROR_IMAGE_COPY_UNSUCCESSFUL = 1829
-        self.PNP_ERROR_FILE_NOT_FOUND = 1803
-
-    def readable(self, error_code: int):
-        return self.__readable.get(error_code, f'unknown: {error_code}')
-
-
-class PnpFlow:
-    __readable = {
-        0: 'None',
-        1: 'new device',
-        2: 'info',
-
-        10: 'update required',
-        11: 'update started',
-        12: 'no update required/done',
-        13: 'update done -> reloading',
-
-        21: 'config start',
-        22: 'config down -> reloading',
-        23: 'reload for config update',
-
-        99: 'finished',
-    }
-
-    def __init__(self):
-        self.NONE = 0
-        self.NEW = 1
-        self.INFO = 2
-
-        self.UPDATE_NEEDED = 10
-        self.UPDATE_START = 11
-        self.UPDATE_DOWN = 12
-        self.UPDATE_RELOAD = 13
-
-        self.CONFIG_START = 21
-        self.CONFIG_DOWN = 22
-        self.CONFIG_RELOAD = 23
-
-        self.FINISHED = 99
-
-    def readable(self, state: int):
-        return self.__readable.get(state, 'unknown')
-
-
-class Device:
-    def __init__(self, udi: str, platform: str, hw_rev: str, serial: str, first_seen: str, last_contact: str,
-                 src_address: str, current_job: str):
-
-        self.udi: str = udi
-        self.platform: str = platform
-        self.hw_rev: str = hw_rev
-        self.serial: str = serial
-        self.ip_address: str = src_address
-        self.current_job: str = current_job
-        self.first_seen: str = first_seen
-        self.last_contact: str = last_contact
-
-        self.version: str = ''
-        self.image: str = ''
-        self.destination_name: str = ''
-        self.destination_free: Optional[int] = None
-        self.status: str = ''
-        self.__pnp_flow: int = PNPFLOW.NEW
-        self.pnp_flow_readable: str = PNPFLOW.readable(PNPFLOW.NEW)
-        self.target_image: Optional[SoftwareImage] = None
-        self.backoff: bool = False
-        self.__refresh_data: bool = False
-        self.refresh_button: str = ''
-        self.__error_code: int = 0
-        self.error_code_readable: str = ERROR.readable(ERROR.ERROR_NO_ERROR)
-        self.error_count: int = 0
-        self.error_message = ''
-        self.__hard_error: bool = False
-        self.__status_class: str = ''
-
-    @property
-    def pnp_flow(self) -> int:
-        return self.__pnp_flow
-
-    @pnp_flow.setter
-    def pnp_flow(self, pnp_flow: int):
-        self.__pnp_flow = pnp_flow
-        self.pnp_flow_readable = PNPFLOW.readable(pnp_flow)
-        if pnp_flow == PNPFLOW.FINISHED:
-            self.__status_class = 'finished'
-
-    @property
-    def refresh_data(self) -> bool:
-        return self.__refresh_data
-
-    @refresh_data.setter
-    def refresh_data(self, new_state: bool):
-        self.__refresh_data = new_state
-        if new_state:
-            self.refresh_button = 'disabled='
-        else:
-            self.refresh_button = 'enabled='
-
-    @property
-    def error_code(self) -> int:
-        return self.__error_code
-
-    @error_code.setter
-    def error_code(self, error_code: int):
-        self.__error_code = error_code
-        self.error_code_readable = ERROR.readable(error_code)
-        self.__status_class = 'warning'
-
-    @property
-    def hard_error(self) -> bool:
-        return self.__hard_error
-
-    @hard_error.setter
-    def hard_error(self, hard_error: bool):
-        self.__hard_error = hard_error
-        self.__status_class = 'error'
-
-    @property
-    def status_class(self) -> str:
-        return self.__status_class
-
-    @status_class.setter
-    def status_class(self, status_class: str):
-        self.__status_class = status_class
-
-
-class Images:
-    def __init__(self, images_file):
-        self.__images = {}
-        self.load_image_data(images_file)
-
-    def load_image_data(self, images_file):
-        try:
-            with open(images_file, 'rb') as f:
-                self.__images = toml_load(f)
-        except FileNotFoundError as e:
-            print(f'ERROR: Data file {images_file} not found! ({e})')
-            exit(1)
-        except TOMLDecodeError as e:
-            print(f'ERROR: Data file {images_file} is not in valid toml format! ({e})')
-            exit(2)
-
-    @property
-    def images(self) -> Dict[str, any]:
-        return self.__images
-
-
-def configure_logger(path):
-    log_formatter = logging.Formatter('%(asctime)s :: %(levelname)s :: %(name)s :: %(module)s ::%(message)s')
-    log = logging.getLogger('root')
-    log.setLevel(logging.INFO)
-
-    log_file = path
-    # create a new file > 5 mb size
-    log_handler_file = RotatingFileHandler(
-        log_file,
-        mode='a',
-        maxBytes=5 * 1024 * 1024,
-        backupCount=10,
-        # encoding=None,
-        # delay=0
-    )
-
-    log_handler_file.setFormatter(log_formatter)
-    log_handler_file.setLevel(logging.INFO)
-    log.addHandler(log_handler_file)
-
-    if SETTINGS.log_to_console:
-        log_handler_console = logging.StreamHandler(stdout)
-        log_handler_console.setFormatter(log_formatter)
-        log_handler_console.setLevel(logging.INFO)
-        log.addHandler(log_handler_console)
 
+from open_pnp_classes import (
+    Device,
+    SoftwareImage,
+    ErrorCodes,
+    PnpFlow,
+    Settings,
+    Images
+)
 
-def log_info(message):
-    if SETTINGS.debug:
-        log = logging.getLogger('root')
-        log.info(message)
+from open_pnp_utils import (
+    configure_logger,
+    log_info,
+    parse_arguments,
+    get_local_ip_addresses,
+)
 
 
-def log_critical(message):
-    if SETTINGS.debug:
-        log = logging.getLogger('root')
-        log.critical(message)
-
-
-def parse_arguments() -> arg_Namespace:
-    parser = ArgumentParser(
-        prog='open-pnp.py',
-        description='This is a basic implementation of the Cisco PnP protocol. It is intended to'
-                    '\nroll out image updates and configurations for Cisco IOS/IOS-XE devices on day0.'
-                    '\n'
-                    f'\n{PNP_SERVER_VERSION} | Written by: thl-cmk, for more information see: https://thl-cmk.hopto.org',
-        formatter_class=RawTextHelpFormatter,
-        epilog='Usage: python open-pnp.py --config_url  http://192.168.10.133:8080/configs '
-               '--image_url http://192.168.10.133:8080/images',
-    )
-    parser.add_argument('-b', '--bind_pnp_server', type=str,
-                        help='Bind PnP server to IP-address. (default: 0.0.0.0)')
-    parser.add_argument('-p', '--port', type=int,
-                        help='TCP port to listen on. (default: 8080)')
-    parser.add_argument('-r', '--status_refresh', type=int,
-                        help='Time in seconds to refresh PnP server status page. (default: 60)')
-    parser.add_argument('-v', '--version', default=False, action='store_const', const=True,
-                        help='Print open-pnp-server version and exit')
-    parser.add_argument('--config_file', type=str,
-                        help='Path/name of open PnP server config file. (default: open-pnp.toml)')
-    parser.add_argument('--config_url', type=str,
-                        help='Download URL for config files. I.e. http://192.168.10.133:8080/configs')
-    parser.add_argument('--image_data', type=str,
-                        help='File containing the image description. (default: images.toml)')
-    parser.add_argument('--image_url', type=str,
-                        help='Download URL for image files. I.e. http://192.168.10.133:8080/images')
-    parser.add_argument('--debug', default=False, action='store_const', const=True,
-                        help='Enable Debug output send to "log_file".')
-    parser.add_argument('--default_cfg', type=str,
-                        help='default config to use if no device specific config is found. (default: DEFAULT.cfg)')
-    parser.add_argument('--log_file', type=str,
-                        help='Path/name of the logfile. (default: log/pnp_debug.log, requires --debug) ')
-    parser.add_argument('--log_to_console', default=False, action='store_const', const=True,
-                        help='Enable debug output send to stdout (requires --debug).')
-    parser.add_argument('--time_format', type=str,
-                        help='Format string to render time. (default: %%Y-%%m-%%dT%%H:%%M:%%S)')
-
-    return parser.parse_args()
+PNP_SERVER_VERSION = '20230223.v1.0.1'
 
 
 def pnp_device_info(udi: str, correlator: str, info_type: str) -> str:
@@ -447,7 +91,7 @@ def pnp_device_info(udi: str, correlator: str, info_type: str) -> str:
         'info_type': info_type
     }
     _template = render_template('device_info.xml', **jinja_context)
-    log_info(_template)
+    log_info(_template, SETTINGS.debug)
     return _template
 
 
@@ -466,7 +110,7 @@ def pnp_backoff(udi: str, correlator: str, minutes: Optional[int] = 1) -> str:
         'hours': hours,
     }
     _template = render_template('backoff.xml', **jinja_context)
-    log_info(_template)
+    log_info(_template, SETTINGS.debug)
     return _template
 
 
@@ -481,7 +125,7 @@ def pnp_backoff_terminate(udi: str, correlator: str) -> str:
         'correlator': correlator,
     }
     _template = render_template('backoff_terminate.xml', **jinja_context)
-    log_info(_template)
+    log_info(_template, SETTINGS.debug)
     return _template
 
 
@@ -503,7 +147,7 @@ def pnp_install_image(udi: str, correlator: str) -> Optional[str]:
             'delay': 0,  # reload in seconds
         }
         _template = render_template('image_install.xml', **jinja_context)
-        log_info(_template)
+        log_info(_template, SETTINGS.debug)
         return _template
     else:
         device.error_code = ERROR.ERROR_NO_IMAGE_FILE
@@ -528,12 +172,33 @@ def pnp_config_upgrade(udi: str, correlator: str) -> Optional[str]:
         'udi': udi,
         'correlator': correlator,
         'base_url': SETTINGS.config_url,
-        'serial_number': cfg_file,
-        'delay': 30,  # reload in seconds
+        'reload_delay': 30,  # reload in seconds
         'cfg_file': cfg_file,
     }
     _template = render_template('config_upgrade.xml', **jinja_context)
-    log_info(_template)
+    log_info(_template, SETTINGS.debug)
+    return _template
+
+
+def pnp_remove_inactive(udi: str, correlator: str) -> Optional[str]:
+    device = devices[udi]
+    cfg_file = 'REMOVE_INACTIVE.cfg'
+    response = head(f'{SETTINGS.config_url}/{cfg_file}')
+    if response.status_code != 200:  # cfg not found
+        device.error_code = ERROR.ERROR_NO_CFG_FILE
+        device.hard_error = True
+        return
+
+    device.current_job = 'urn:cisco:pnp:config-upgrade'
+    device.pnp_flow = PNPFLOW.CLEANUP_START
+    jinja_context = {
+        'udi': udi,
+        'correlator': correlator,
+        'base_url': SETTINGS.config_url,
+        'cfg_file': cfg_file,
+    }
+    _template = render_template('config_upgrade.xml', **jinja_context)
+    log_info(_template, SETTINGS.debug)
     return _template
 
 
@@ -543,7 +208,7 @@ def pnp_bye(udi: str, correlator: str) -> str:
         'correlator': correlator,
     }
     _template = render_template('bye.xml', **jinja_context)
-    log_info(_template)
+    log_info(_template, SETTINGS.debug)
     return _template
 
 
@@ -614,26 +279,6 @@ def check_update(udi: str):
             device.error_code = ERROR.ERROR_NO_FREE_SPACE
             device.hard_error = True
 
-
-def get_local_ip_addresses() -> List[str]:
-    _addresses = []
-    adapters = get_adapters()
-    for adapter in adapters:
-        ip_addr = ''
-        for ip in adapter.ips:
-            drop_ip = False
-            if ip.is_IPv4:
-                ip_addr = ip.ip
-            elif ip.is_IPv6:
-                ip_addr = ip.ip[0]
-            for entry in ['127.', 'fe80::', '::1']:
-                if str(ip_addr).lower().startswith(entry):
-                    drop_ip = True
-            if not drop_ip:
-                _addresses.append(ip_addr)
-    return _addresses
-
-
 # flask
 app = Flask(__name__, template_folder='./templates')
 
@@ -699,7 +344,7 @@ def pnp_hello():
 def pnp_work_request():
     src_add = request.environ.get('HTTP_X_REAL_IP', request.remote_addr)
     data = xml_parse(request.data)
-    log_info(f'REQUEST: {data}')
+    log_info(f'REQUEST: {data}', SETTINGS.debug)
     correlator = data['pnp']['info']['@correlator']
     udi = data['pnp']['@udi']
     if udi in devices.keys():
@@ -710,7 +355,7 @@ def pnp_work_request():
             return Response(pnp_backoff(udi, correlator, 10), mimetype='text/xml')
             pass
         if device.backoff:
-            log_info('BACKOFF')
+            log_info('BACKOFF', SETTINGS.debug)
             # backoff more and more on errors, max error_count = 11 -> 5 * 11 = 55
             # error_count == 12 -> like hard_error
             minutes = device.error_count + 1
@@ -718,26 +363,27 @@ def pnp_work_request():
                 device.hard_error = True
             return Response(pnp_backoff(udi, correlator, minutes), mimetype='text/xml')
         if device.pnp_flow == PNPFLOW.NEW:
-            log_info('PNPFLOW.NEW')
+            log_info('PNPFLOW.NEW', SETTINGS.debug)
             device.pnp_flow = PNPFLOW.INFO
             return Response(pnp_device_info(udi, correlator, 'all'), mimetype='text/xml')
         if device.pnp_flow == PNPFLOW.UPDATE_NEEDED:
-            log_info('PNPFLOW.UPDATE_NEEDED')
+            log_info('PNPFLOW.UPDATE_NEEDED', SETTINGS.debug)
             device.pnp_flow = PNPFLOW.UPDATE_START
             return Response(pnp_install_image(udi, correlator), mimetype='text/xml')
         if device.pnp_flow == PNPFLOW.UPDATE_RELOAD:
-            log_info('PNPFLOW.UPDATE_RELOAD')
+            log_info('PNPFLOW.UPDATE_RELOAD', SETTINGS.debug)
             return Response(pnp_device_info(udi, correlator, 'all'), mimetype='text/xml')
         if device.pnp_flow == PNPFLOW.UPDATE_DOWN:
-            log_info('PNPFLOW.UPDATE_DOWN')
+            log_info('PNPFLOW.UPDATE_DOWN', SETTINGS.debug)
             return Response(pnp_config_upgrade(udi, correlator), mimetype='text/xml')
         if device.pnp_flow == PNPFLOW.CONFIG_DOWN:  # will never reach this point, as pnp is removed bei EEM :-)
-            log_info('PNPFLOW.CONFIG_DOWN')
+            log_info('PNPFLOW.CONFIG_DOWN', SETTINGS.debug)
             return Response(pnp_backoff_terminate(udi, correlator), mimetype='text/xml')
-        log_info(f'Other PNP_FLOW: {PNPFLOW.readable(device.pnp_flow)}')
+        log_info(
+            f'Other PNP_FLOW: {PNPFLOW.readable(device.pnp_flow)}', SETTINGS.debug)
         return Response('', 200)
     else:
-        log_info('REQUEST NEW DEVICE FOUND')
+        log_info('REQUEST NEW DEVICE FOUND', SETTINGS.debug)
         create_new_device(udi, src_add)
         # return Response(device_info(udi, correlator, 'all'), mimetype='text/xml')
         devices[udi].pnp_flow = PNPFLOW.NEW
@@ -748,7 +394,7 @@ def pnp_work_request():
 @app.route('/pnp/WORK-RESPONSE', methods=['POST'])
 def pnp_work_response():
     data = xml_parse(request.data)
-    log_info(f'RESPONSE: {data}')
+    log_info(f'RESPONSE: {data}', SETTINGS.debug)
     src_add = request.environ.get('HTTP_X_REAL_IP', request.remote_addr)
     udi = data['pnp']['@udi']
     job_type = data['pnp']['response']['@xmlns']
@@ -764,10 +410,10 @@ def pnp_work_response():
     else:
         correlator = data['pnp']['response']['@correlator']
         job_status = int(data['pnp']['response']['@success'])
-        log_info(f'Correlator: {correlator}')
-        log_info(f'Job type: {job_type}')
-        log_info(f'PnP flow: {device.pnp_flow_readable}')
-        log_info(f'Job status: {job_status}')
+        log_info(f'Correlator: {correlator}', SETTINGS.debug)
+        log_info(f'Job type: {job_type}', SETTINGS.debug)
+        log_info(f'PnP flow: {device.pnp_flow_readable}', SETTINGS.debug)
+        log_info(f'Job status: {job_status}', SETTINGS.debug)
         if job_status == 1:  # success
             if job_type not in ['urn:cisco:pnp:backoff']:
                 device.backoff = True
@@ -788,7 +434,7 @@ def pnp_work_response():
                 # device.pnp_flow = PNPFLOW.INFO
                 pass
             _response = pnp_bye(udi, correlator)
-            log_info(_response)
+            log_info(_response, SETTINGS.debug)
             return Response(_response, mimetype='text/xml')
         elif job_status == 0:
             error_code = int(data['pnp']['response']['errorInfo']['errorCode'].split(' ')[-1])
@@ -799,7 +445,7 @@ def pnp_work_response():
                 device.hard_error = True
             return Response(pnp_bye(udi, correlator), mimetype='text/xml')
     device.current_job = 'none'
-    log_info('Empty Response')
+    log_info('Empty Response', SETTINGS.debug)
     return Response('')
 
 
@@ -807,7 +453,7 @@ if __name__ == '__main__':
 
     ERROR = ErrorCodes()
     PNPFLOW = PnpFlow()
-    SETTINGS = Settings(vars(parse_arguments()))
+    SETTINGS = Settings(vars(parse_arguments(PNP_SERVER_VERSION)))
     SETTINGS.update(SETTINGS.cfg_file)
 
     if SETTINGS.version:
@@ -822,7 +468,7 @@ if __name__ == '__main__':
         app.debug = True
     else:
         # disable FLASK console output
-        logging.getLogger("werkzeug").disabled = True
+        getLogger("werkzeug").disabled = True
         cli.show_server_banner = lambda *args: None
 
     if SETTINGS.image_url == '':
@@ -833,8 +479,8 @@ if __name__ == '__main__':
         exit(1)
 
     if SETTINGS.debug:
-        configure_logger(SETTINGS.log_file)
-        log_info('STARTED LOGGER')
+        configure_logger(SETTINGS.log_file, SETTINGS.log_to_console)
+        log_info('STARTED LOGGER', SETTINGS.debug)
 
     print()
     print(f'Running open PnP server. Stop with ctrl+c')
diff --git a/pnp/open_pnp_classes.py b/pnp/open_pnp_classes.py
new file mode 100644
index 0000000000000000000000000000000000000000..5c326298caded6e484b3e437784f1177d81d8f2d
--- /dev/null
+++ b/pnp/open_pnp_classes.py
@@ -0,0 +1,319 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# License: GNU General Public License v2
+#
+# Author: thl-cmk[at]outlook[dot]com
+# URL   : https://thl-cmk.hopto.org
+# Date  : 2023-02-26
+# File  : open_pnp_classes.py
+
+
+from typing import Dict, Optional, Any
+from tomli import load as toml_load
+from tomli import TOMLDecodeError
+
+
+class Settings:
+    def __init__(
+            self,
+            cli_args: Dict[str, Any],
+            version: bool = False,
+            cfg_file: Optional[str] = 'open-pnp.toml',
+            image_data: Optional[str] = 'images.toml',
+            bind_pnp_server: Optional[str] = '0.0.0.0',
+            port: Optional[int] = 8080,
+            time_format: Optional[str] = '%Y-%m-%dT%H:%M:%S',
+            status_refresh: Optional[int] = 60,
+            debug: Optional[bool] = False,
+            log_to_console: Optional[bool] = False,
+            log_file: Optional[str] = 'log/pnp_debug.log',
+            image_url: Optional[str] = '',
+            config_url: Optional[str] = '',
+            default_cfg: Optional[str] = 'DEFAULT.cfg',
+    ):
+        self.__settings = {
+            'cfg_file': cfg_file,
+            'version': version,
+            'image_data': image_data,
+            'bind_pnp_server': bind_pnp_server,
+            'port': port,
+            'time_format': time_format,
+            'status_refresh': status_refresh,
+            'debug': debug,
+            'log_to_console': log_to_console,
+            'log_file': log_file,
+            'image_url': image_url,
+            'config_url': config_url,
+            'default_cfg_file': default_cfg,
+        }
+        self.__args = {}
+        self.__set_cli_args(cli_args)
+
+    def __set_cli_args(self, cli_args: Dict[str, Any]):
+        self.__args = ({k: v for k, v in cli_args.items() if v})
+        self.__settings.update(self.__args)
+
+    def update(self, cfg_file: str):
+        try:
+            with open(cfg_file, 'rb') as f:
+                self.__settings.update(toml_load(f))
+        except FileNotFoundError as e:
+            print(f'ERROR: Data file {cfg_file} not found! ({e})')
+            exit(1)
+        except TOMLDecodeError as e:
+            print(
+                f'ERROR: Data file {cfg_file} is not in valid toml format! ({e})')
+            exit(2)
+
+        self.__settings.update(self.__args)
+
+    @property
+    def cfg_file(self) -> str:
+        return self.__settings['cfg_file']
+
+    @property
+    def version(self) -> bool:
+        return self.__settings['version']
+
+    @property
+    def image_data(self) -> str:
+        return self.__settings['image_data']
+
+    @property
+    def bind_pnp_server(self) -> str:
+        return self.__settings['bind_pnp_server']
+
+    @property
+    def port(self) -> int:
+        return self.__settings['port']
+
+    @property
+    def time_format(self) -> str:
+        return self.__settings['time_format']
+
+    @property
+    def status_refresh(self) -> int:
+        return self.__settings['status_refresh']
+
+    @property
+    def debug(self) -> bool:
+        return self.__settings['debug']
+
+    @property
+    def log_to_console(self) -> bool:
+        return self.__settings['log_to_console']
+
+    @property
+    def log_file(self) -> str:
+        return self.__settings['log_file']
+
+    @property
+    def image_url(self) -> str:
+        return self.__settings['image_url']
+
+    @property
+    def config_url(self) -> str:
+        return self.__settings['config_url']
+
+    @property
+    def default_cfg(self) -> str:
+        return self.__settings['default_cfg']
+
+
+class SoftwareImage:
+    def __init__(self, image: str, version: str, md5: str, size: int,):
+        self.image: str = image
+        self.version: str = version
+        self.md5: str = md5
+        self.size: int = size
+
+
+class ErrorCodes:
+    __readable = {
+        0: 'No error',
+        100: 'unknown platform',
+        101: 'no free space for update',
+        102: 'unknown image',
+        103: 'config file not found',
+        104: 'image file not found',
+
+        1412: 'Invalid input detected (config)',
+        1413: 'Invalid input detected',
+        1609: 'Error while retrieving device filesystem info',
+        1816: 'Error verifying checksum for Image',
+        1829: 'Image copy was unsuccessful',
+        1803: 'Source file not found',
+    }
+
+    def __init__(self):
+        self.ERROR_NO_ERROR = 0
+
+        self.ERROR_NO_PLATFORM = 100
+        self.ERROR_NO_FREE_SPACE = 101
+        self.ERROR_NO_IMAGE = 102
+        self.ERROR_NO_CFG_FILE = 103
+        self.ERROR_NO_IMAGE_FILE = 104
+
+        self.PNP_ERROR_INVALID_CONFIG = 1412
+        self.PNP_ERROR_INVALID_INPUT = 1413
+        self.PNP_ERROR_NO_FILESYSTEM_INFO = 1609
+        self.PNP_ERROR_BAD_CHECKSUM = 1816
+        self.PNP_ERROR_IMAGE_COPY_UNSUCCESSFUL = 1829
+        self.PNP_ERROR_FILE_NOT_FOUND = 1803
+
+    def readable(self, error_code: int):
+        return self.__readable.get(error_code, f'unknown: {error_code}')
+
+
+class PnpFlow:
+    __readable = {
+        0: 'None',
+        1: 'new device',
+        2: 'info',
+
+        10: 'update required',
+        11: 'update started',
+        12: 'no update required/done',
+        13: 'update done -> reloading',
+
+        21: 'config start',
+        22: 'config down -> reloading',
+        23: 'reload for config update',
+
+        30: 'cleanup required',
+        31: 'cleanup started',
+        32: 'no cleanup required/done',
+
+        99: 'finished',
+    }
+
+    def __init__(self):
+        self.NONE = 0
+        self.NEW = 1
+        self.INFO = 2
+
+        self.UPDATE_NEEDED = 10
+        self.UPDATE_START = 11
+        self.UPDATE_DOWN = 12
+        self.UPDATE_RELOAD = 13
+
+        self.CONFIG_START = 21
+        self.CONFIG_DOWN = 22
+        self.CONFIG_RELOAD = 23
+
+        self.CLEANUP_NEEDED = 30
+        self.CLEANUP_START = 31
+        self.CLEANUP_DOWN = 32
+
+        self.FINISHED = 99
+
+    def readable(self, state: int):
+        return self.__readable.get(state, 'unknown')
+
+
+class Device:
+    def __init__(self, udi: str, platform: str, hw_rev: str, serial: str, first_seen: str, last_contact: str,
+                 src_address: str, current_job: str):
+
+        self.udi: str = udi
+        self.platform: str = platform
+        self.hw_rev: str = hw_rev
+        self.serial: str = serial
+        self.ip_address: str = src_address
+        self.current_job: str = current_job
+        self.first_seen: str = first_seen
+        self.last_contact: str = last_contact
+
+        self.version: str = ''
+        self.image: str = ''
+        self.destination_name: str = ''
+        self.destination_free: Optional[int] = None
+        self.status: str = ''
+        self.__pnp_flow: int = PNPFLOW.NEW
+        self.pnp_flow_readable: str = PNPFLOW.readable(PNPFLOW.NEW)
+        self.target_image: Optional[SoftwareImage] = None
+        self.backoff: bool = False
+        self.__refresh_data: bool = False
+        self.refresh_button: str = ''
+        self.__error_code: int = 0
+        self.error_code_readable: str = ERROR.readable(ERROR.ERROR_NO_ERROR)
+        self.error_count: int = 0
+        self.error_message = ''
+        self.__hard_error: bool = False
+        self.__status_class: str = ''
+
+    @property
+    def pnp_flow(self) -> int:
+        return self.__pnp_flow
+
+    @pnp_flow.setter
+    def pnp_flow(self, pnp_flow: int):
+        self.__pnp_flow = pnp_flow
+        self.pnp_flow_readable = PNPFLOW.readable(pnp_flow)
+        if pnp_flow == PNPFLOW.FINISHED:
+            self.__status_class = 'finished'
+
+    @property
+    def refresh_data(self) -> bool:
+        return self.__refresh_data
+
+    @refresh_data.setter
+    def refresh_data(self, new_state: bool):
+        self.__refresh_data = new_state
+        if new_state:
+            self.refresh_button = 'disabled='
+        else:
+            self.refresh_button = 'enabled='
+
+    @property
+    def error_code(self) -> int:
+        return self.__error_code
+
+    @error_code.setter
+    def error_code(self, error_code: int):
+        self.__error_code = error_code
+        self.error_code_readable = ERROR.readable(error_code)
+        self.__status_class = 'warning'
+
+    @property
+    def hard_error(self) -> bool:
+        return self.__hard_error
+
+    @hard_error.setter
+    def hard_error(self, hard_error: bool):
+        self.__hard_error = hard_error
+        self.__status_class = 'error'
+
+    @property
+    def status_class(self) -> str:
+        return self.__status_class
+
+    @status_class.setter
+    def status_class(self, status_class: str):
+        self.__status_class = status_class
+
+
+class Images:
+    def __init__(self, images_file):
+        self.__images = {}
+        self.load_image_data(images_file)
+
+    def load_image_data(self, images_file):
+        try:
+            with open(images_file, 'rb') as f:
+                self.__images = toml_load(f)
+        except FileNotFoundError as e:
+            print(f'ERROR: Data file {images_file} not found! ({e})')
+            exit(1)
+        except TOMLDecodeError as e:
+            print(
+                f'ERROR: Data file {images_file} is not in valid toml format! ({e})')
+            exit(2)
+
+    @property
+    def images(self) -> Dict[str, Any]:
+        return self.__images
+
+ERROR = ErrorCodes()
+PNPFLOW = PnpFlow()
diff --git a/pnp/open_pnp_utils.py b/pnp/open_pnp_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..ecbc90a801846e84bc920a672c8b3bd896a27aff
--- /dev/null
+++ b/pnp/open_pnp_utils.py
@@ -0,0 +1,120 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# License: GNU General Public License v2
+#
+# Author: thl-cmk[at]outlook[dot]com
+# URL   : https://thl-cmk.hopto.org
+# Date  : 2023-02-26
+# File  : open_pnp_utils.py
+
+from typing import List
+import logging
+from logging.handlers import RotatingFileHandler
+from sys import stdout
+from argparse import (
+    Namespace as arg_Namespace,
+    ArgumentParser,
+    RawTextHelpFormatter,
+)
+from ifaddr import get_adapters
+
+
+def configure_logger(path: str, log_to_console: bool):
+    log_formatter = logging.Formatter(
+        '%(asctime)s :: %(levelname)s :: %(name)s :: %(module)s ::%(message)s')
+    log = logging.getLogger('root')
+    log.setLevel(logging.INFO)
+
+    log_file = path
+    # create a new file > 5 mb size
+    log_handler_file = RotatingFileHandler(
+        log_file,
+        mode='a',
+        maxBytes=5 * 1024 * 1024,
+        backupCount=10,
+        # encoding=None,
+        # delay=0
+    )
+
+    log_handler_file.setFormatter(log_formatter)
+    log_handler_file.setLevel(logging.INFO)
+    log.addHandler(log_handler_file)
+
+    if log_to_console:
+        log_handler_console = logging.StreamHandler(stdout)
+        log_handler_console.setFormatter(log_formatter)
+        log_handler_console.setLevel(logging.INFO)
+        log.addHandler(log_handler_console)
+
+
+def log_info(message: str, debug: bool):
+    if debug:
+        log = logging.getLogger('root')
+        log.info(message)
+
+
+def log_critical(message: str, debug: bool):
+    if debug:
+        log = logging.getLogger('root')
+        log.critical(message)
+
+
+def parse_arguments(PNP_SERVER_VERSION: str) -> arg_Namespace:
+    parser = ArgumentParser(
+        prog='open-pnp.py',
+        description='This is a basic implementation of the Cisco PnP protocol. It is intended to'
+                    '\nroll out image updates and configurations for Cisco IOS/IOS-XE devices on day0.'
+                    '\n'
+                    f'\n{PNP_SERVER_VERSION} | Written by: thl-cmk, for more information see: https://thl-cmk.hopto.org',
+        formatter_class=RawTextHelpFormatter,
+        epilog='Usage: python open-pnp.py --config_url  http://192.168.10.133:8080/configs '
+               '--image_url http://192.168.10.133:8080/images',
+    )
+    parser.add_argument('-b', '--bind_pnp_server', type=str,
+                        help='Bind PnP server to IP-address. (default: 0.0.0.0)')
+    parser.add_argument('-p', '--port', type=int,
+                        help='TCP port to listen on. (default: 8080)')
+    parser.add_argument('-r', '--status_refresh', type=int,
+                        help='Time in seconds to refresh PnP server status page. (default: 60)')
+    parser.add_argument('-v', '--version', default=False, action='store_const', const=True,
+                        help='Print open-pnp-server version and exit')
+    parser.add_argument('--config_file', type=str,
+                        help='Path/name of open PnP server config file. (default: open-pnp.toml)')
+    parser.add_argument('--config_url', type=str,
+                        help='Download URL for config files. I.e. http://192.168.10.133:8080/configs')
+    parser.add_argument('--image_data', type=str,
+                        help='File containing the image description. (default: images.toml)')
+    parser.add_argument('--image_url', type=str,
+                        help='Download URL for image files. I.e. http://192.168.10.133:8080/images')
+    parser.add_argument('--debug', default=False, action='store_const', const=True,
+                        help='Enable Debug output send to "log_file".')
+    parser.add_argument('--default_cfg', type=str,
+                        help='default config to use if no device specific config is found. (default: DEFAULT.cfg)')
+    parser.add_argument('--log_file', type=str,
+                        help='Path/name of the logfile. (default: log/pnp_debug.log, requires --debug) ')
+    parser.add_argument('--log_to_console', default=False, action='store_const', const=True,
+                        help='Enable debug output send to stdout (requires --debug).')
+    parser.add_argument('--time_format', type=str,
+                        help='Format string to render time. (default: %%Y-%%m-%%dT%%H:%%M:%%S)')
+
+    return parser.parse_args()
+
+
+def get_local_ip_addresses() -> List[str]:
+    _addresses = []
+    adapters = get_adapters()
+    for adapter in adapters:
+        ip_addr = ''
+        for ip in adapter.ips:
+            drop_ip = False
+            if ip.is_IPv4:
+                ip_addr = ip.ip
+            elif ip.is_IPv6:
+                ip_addr = ip.ip[0]
+            for entry in ['127.', 'fe80::', '::1']:
+                if str(ip_addr).lower().startswith(entry):
+                    drop_ip = True
+            if not drop_ip:
+                _addresses.append(ip_addr)
+    return _addresses
diff --git a/pnp/templates/config_upgrade.xml b/pnp/templates/config_upgrade.xml
index c5ccdf10b2fc16cfac134bba1d040b1e4a69abab..a6d5dc6f1a84b0d8dfbf41cdb402c801edeca459 100644
--- a/pnp/templates/config_upgrade.xml
+++ b/pnp/templates/config_upgrade.xml
@@ -10,12 +10,15 @@
             <!-- <applyto>startup</applyto> -->  <!-- don't now where to place this -->
             <!-- <abortonsyntaxfault/> -->       <!-- don't now where to place this -->
         </config>
+        {% if reload_delay is defined %}
         <reload>
             <reason>pnp device config</reason>
-            <delayIn>{{ delay }}</delayIn>
+            <delayIn>{{ reload_delay }}</delayIn>
             <user>pnp-device-config</user>
             <saveConfig>true</saveConfig>
         </reload>
-<!--        <noReload/>-->
+        {% else %}
+            <noReload/>
+        {% endif %}
     </request>
-</pnp>
\ No newline at end of file
+</pnp>