motom001/DoorPi

View on GitHub
doorpi/doorpi.py

Summary

Maintainability
C
1 day
Test Coverage
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import logging
logger = logging.getLogger(__name__)
logger.debug("%s loaded", __name__)

import sys
import argparse

import time  # used by: DoorPi.run
import os  # used by: DoorPi.load_config

import datetime  # used by: parse_string
import cgi  # used by: parse_string
import tempfile

import metadata
from keyboard.KeyboardInterface import load_keyboard
from sipphone.SipphoneInterface import load_sipphone
from status.webserver import load_webserver
from conf.config_object import ConfigObject
from action.handler import EventHandler
from status.status_class import DoorPiStatus
#from status.webservice import run_webservice, WebService
from action.base import SingleAction


class DoorPiShutdownAction(SingleAction): pass
class DoorPiNotExistsException(Exception): pass
class DoorPiEventHandlerNotExistsException(Exception): pass
class DoorPiRestartException(Exception): pass

class Singleton(type):
    _instances = {}
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
        return cls._instances[cls]

class DoorPi(object):
    __metaclass__ = Singleton

    __prepared = False

    __config = None
    @property
    def config(self): return self.__config

    __keyboard = None
    @property
    def keyboard(self): return self.__keyboard
    #def get_keyboard(self): return self.__keyboard

    __sipphone = None
    @property
    def sipphone(self): return self.__sipphone

    @property
    def additional_informations(self):
        if self.event_handler is None: return {}
        else: return self.event_handler.additional_informations

    __event_handler = None
    @property
    def event_handler(self): return self.__event_handler

    __webserver = None
    @property
    def webserver(self): return self.__webserver

    @property
    def status(self): return DoorPiStatus(self)
    def get_status(self, modules = '', value= '', name = ''): return DoorPiStatus(self, modules, value, name)

    @property
    def epilog(self): return metadata.epilog

    @property
    def name(self): return str(metadata.package)
    @property
    def name_and_version(self): return str(metadata.package) + " - version: " + metadata.version

    __shutdown = False
    @property
    def shutdown(self): return self.__shutdown

    _base_path = metadata.doorpi_path
    @property
    def base_path(self):
        if self._base_path is None:
            try:
                self._base_path = os.path.join(os.path.expanduser('~'), metadata.package)
                assert os.access(self._base_path, os.W_OK), 'use fallback for base_path (see tmp path)'
            except Exception as exp:
                logger.error(exp)
                import tempfile
                self._base_path = tempfile.gettempdir()
        return self._base_path

    def __init__(self, parsed_arguments = None):
        self.__parsed_arguments = parsed_arguments
        # for start as daemon - if start as app it will not matter to load this vars
        self.stdin_path = '/dev/null'
        self.stdout_path = '/dev/null'
        self.stderr_path = '/dev/null'
        self.pidfile_path =  '/var/run/doorpi.pid'
        self.pidfile_timeout = 5

        self.__last_tick = time.time()

    def doorpi_shutdown(self, time_until_shutdown=10):
        time.sleep(time_until_shutdown)
        self.__shutdown = True

    def prepare(self, parsed_arguments):
        logger.debug("prepare")
        logger.debug("given arguments argv: %s", parsed_arguments)

        self.__config = ConfigObject.load_config(parsed_arguments.configfile)
        self._base_path = self.config.get('DoorPi', 'base_path', self.base_path)
        self.__event_handler = EventHandler()

        if self.config.config_file is None:
            self.event_handler.register_action('AfterStartup', self.config.save_config)
            self.config.get('EVENT_OnStartup', '10', 'sleep:1')

        if 'test' in parsed_arguments and parsed_arguments.test is True:
            logger.warning('using only test-mode and destroy after 5 seconds')
            self.event_handler.register_action('AfterStartup', DoorPiShutdownAction(self.doorpi_shutdown))

        # register own events
        self.event_handler.register_event('BeforeStartup', __name__)
        self.event_handler.register_event('OnStartup', __name__)
        self.event_handler.register_event('AfterStartup', __name__)
        self.event_handler.register_event('BeforeShutdown', __name__)
        self.event_handler.register_event('OnShutdown', __name__)
        self.event_handler.register_event('AfterShutdown', __name__)
        self.event_handler.register_event('OnTimeTick', __name__)
        self.event_handler.register_event('OnTimeTickRealtime', __name__)

        # register base actions
        self.event_handler.register_action('OnTimeTick', 'time_tick:!last_tick!')

        # register modules
        self.__webserver    = load_webserver()
        self.__keyboard     = load_keyboard()
        self.__sipphone     = load_sipphone()
        self.sipphone.start()

        # register eventbased actions from configfile
        for event_section in self.config.get_sections('EVENT_'):
            logger.info("found EVENT_ section '%s' in configfile", event_section)
            event_name = event_section[len('EVENT_'):]
            for action in sorted(self.config.get_keys(event_section)):
                logger.info("registering action '%s' for event '%s'", action, event_name)
                self.event_handler.register_action(event_name, self.config.get(event_section, action))

        # register actions for inputpins
        if 'KeyboardHandler' not in self.keyboard.name:
            section_name = 'InputPins'
            for input_pin in sorted(self.config.get_keys(section_name)):
                self.event_handler.register_action('OnKeyPressed_'+input_pin, self.config.get(section_name, input_pin))
        else:
            for keyboard_name in self.keyboard.loaded_keyboards:
                section_name = keyboard_name+'_InputPins'
                for input_pin in self.config.get_keys(section_name, log = False):
                    self.event_handler.register_action(
                        'OnKeyPressed_'+keyboard_name+'.'+input_pin,
                        self.config.get(section_name, input_pin)
                    )

        # register actions for DTMF
        section_name = 'DTMF'
        for DTMF in sorted(self.config.get_keys(section_name)):
            self.event_handler.register_action('OnDTMF_'+DTMF, self.config.get(section_name, DTMF))

        # register keep_alive_led
        is_alive_led = self.config.get('DoorPi', 'is_alive_led', '')
        if is_alive_led is not '':
            self.event_handler.register_action('OnTimeSecondEvenNumber', 'out:%s,HIGH,False'%is_alive_led)
            self.event_handler.register_action('OnTimeSecondUnevenNumber', 'out:%s,LOW,False'%is_alive_led)

        self.__prepared = True
        return self

    def __del__(self):
        return self.destroy()

    @property
    def modules_destroyed(self):
        if len(self.event_handler.sources) > 1: return False
        return self.event_handler.idle

    def destroy(self):
        logger.debug('destroy doorpi')

        if not self.event_handler or self.event_handler.threads == None:
            DoorPiEventHandlerNotExistsException("don't try to stop, when not prepared")
            return False

        logger.debug("Threads before starting shutdown: %s", self.event_handler.threads)

        self.event_handler.fire_event('BeforeShutdown', __name__)
        self.event_handler.fire_event_synchron('OnShutdown', __name__)
        self.event_handler.fire_event('AfterShutdown', __name__)

        timeout = 5
        waiting_between_checks = 0.5
        time.sleep(waiting_between_checks)
        while timeout > 0 and self.modules_destroyed is not True:
            # while not self.event_handler.idle and timeout > 0 and len(self.event_handler.sources) > 1:
            logger.debug('wait %s seconds for threads %s and %s event',
                         timeout, len(self.event_handler.threads[1:]), len(self.event_handler.sources))
            logger.trace('still existing threads:       %s', self.event_handler.threads[1:])
            logger.trace('still existing event sources: %s', self.event_handler.sources)
            time.sleep(waiting_between_checks)
            timeout -= waiting_between_checks

        if timeout <= 0:
            logger.warning("waiting for threads to time out - there are still threads: %s", self.event_handler.threads[1:])

        logger.info('======== DoorPi successfully shutdown ========')
        return True

    def restart(self):
        if self.destroy(): self.run()
        else: raise DoorPiRestartException()

    def run(self):
        logger.debug("run")
        if not self.__prepared: self.prepare(self.__parsed_arguments)

        self.event_handler.fire_event('BeforeStartup', __name__)
        self.event_handler.fire_event_synchron('OnStartup', __name__)
        self.event_handler.fire_event('AfterStartup', __name__)

        # self.event_handler.register_action('OnTimeMinuteUnevenNumber', 'doorpi_restart')

        logger.info('DoorPi started successfully')
        logger.info('BasePath is %s', self.base_path)
        if self.__webserver:
            logger.info('Weburl is %s', self.__webserver.own_url)
        else:
            logger.info('no Webserver loaded')

        time_ticks = 0

        while True and not self.__shutdown:
            time_ticks += 0.05
            self.check_time_critical_threads()
            if time_ticks > 0.5:
                self.__last_tick = time.time()
                self.__event_handler.fire_event_asynchron('OnTimeTick', __name__)
                time_ticks = 0
            time.sleep(0.05)
        return self

    def check_time_critical_threads(self):
        if self.sipphone: self.sipphone.self_check()

    def parse_string(self, input_string):
        parsed_string = datetime.datetime.now().strftime(str(input_string))

        if self.keyboard is None or self.keyboard.last_key is None:
            self.additional_informations['LastKey'] = "NotSetYet"
        else:
            self.additional_informations['LastKey'] = str(self.keyboard.last_key)

        infos_as_html = '<table>'
        for key in self.additional_informations.keys():
            infos_as_html += '<tr><td>'
            infos_as_html += '<b>'+key+'</b>'
            infos_as_html += '</td><td>'
            infos_as_html += '<i>'+cgi.escape(
                str(self.additional_informations.get(key)).replace("\r\n", "<br />")
            )+'</i>'
            infos_as_html += '</td></tr>'
        infos_as_html += '</table>'

        mapping_table = {
            'INFOS_PLAIN':      str(self.additional_informations),
            'INFOS':            infos_as_html,
            'BASEPATH':         self.base_path,
            'last_tick':        str(self.__last_tick)
        }

        for key in metadata.__dict__.keys():
            if isinstance(metadata.__dict__[key], str):
                mapping_table[key.upper()] = metadata.__dict__[key]

        if self.config:
            mapping_table.update({
                'LAST_SNAPSHOT':    str(self.config.get_string('DoorPi', 'last_snapshot', log=False))
            })
        if self.keyboard and 'KeyboardHandler' not in self.keyboard.name:
            for output_pin in self.config.get_keys('OutputPins', log = False):
                mapping_table[self.config.get('OutputPins', output_pin, log = False)] = output_pin
        elif self.keyboard and 'KeyboardHandler' in self.keyboard.name:
            for outputpin_section in self.config.get_sections('_OutputPins', False):
                for output_pin in self.config.get_keys(outputpin_section, log = False):
                    mapping_table[self.config.get(outputpin_section, output_pin, log = False)] = output_pin

        for key in mapping_table.keys():
            parsed_string = parsed_string.replace(
                "!"+key+"!",
                mapping_table[key]
            )

        for key in self.additional_informations.keys():
            parsed_string = parsed_string.replace(
                "!"+key+"!",
                str(self.additional_informations[key])
            )

        return parsed_string

if __name__ == '__main__':
    raise Exception('use main.py to start DoorPi')