Beispiel #1
0
 def get_config(self, key):
     """ Try to get the config over the MQ. If value is None, get the default value
     """
     if self._client_config == None:
         self._client_config = Query(self.zmq, self.log)
     value = self._client_config.query(self._type, self._name, key)
     if value == None or value == 'None':
         self.log.info(u"Value for '{0}' is None or 'None' : trying to get the default value instead...".format(key))
         value = self.get_config_default_value(key)
     self.log.info(u"Value for '{0}' is : {1}".format(key, value))
     return self.cast_config_value(key, value)
Beispiel #2
0
 def check_configured(self):
     """ For a client only
         To be call in the client __init__()
         Check in database (over queryconfig) if the key 'configured' is set to True for the client
         if not, stop the client and log this
     """
     self._client_config = Query(self.zmq, self.log)
     configured = self._client_config.query(self._type, self._name, 'configured')
     if configured == '1':
         configured = True
     if configured != True:
         self.log.error(u"The client is not configured (configured = '{0}'. Stopping the client...".format(configured))
         self.force_leave(status = STATUS_NOT_CONFIGURED)
         return False
     self.log.info(u"The client is configured. Continuing (hoping that the user applied the appropriate configuration ;)")
     return True
Beispiel #3
0
 def get_config(self, key):
     """ Try to get the config over the MQ. If value is None, get the default value
     """
     if self._plugin_config == None:
         self._plugin_config = Query(self.zmq, self.log)
     value = self._plugin_config.query(self._name, key)
     if value == None or value == 'None':
         self.log.info(u"Value for '{0}' is None or 'None' : trying to get the default value instead...".format(key))
         value = self.get_config_default_value(key)
     self.log.info(u"Value for '{0}' is : {1}".format(key, value))
     return self.cast_config_value(key, value)
Beispiel #4
0
 def check_configured(self):
     """ For a plugin only
         To be call in the plugin __init__()
         Check in database (over queryconfig) if the key 'configured' is set to True for the plugin
         if not, stop the plugin and log this
     """
     self._plugin_config = Query(self.zmq, self.log)
     configured = self._plugin_config.query(self._name, 'configured')
     if configured == '1':
         configured = True
     if configured != True:
         self.log.error(u"The plugin is not configured (configured = '{0}'. Stopping the plugin...".format(configured))
         self.force_leave(status = STATUS_NOT_CONFIGURED)
         return False
     self.log.info(u"The plugin is configured. Continuing (hoping that the user applied the appropriate configuration ;)")
     return True
Beispiel #5
0
class Plugin(BasePlugin, MQRep):
    '''
    Global plugin class, manage signal handlers.
    This class shouldn't be used as-it but should be extended by no xPL plugin or by the class xPL plugin which will be used by the xPL plugins
    This class is a Singleton
    '''


    def __init__(self, name, stop_cb = None, is_manager = False, parser = None,
                 daemonize = True, log_prefix = "", test = False):
        '''
        Create XplPlugin instance, which defines system handlers
        @param name : The name of the current plugin
        @param stop_cb : Additionnal method to call when a stop request is received
        @param is_manager : Must be True if the child script is a Domogik Manager process
        You should never need to set it to True unless you develop your own manager
        @param parser : An instance of ArgumentParser. If you want to add extra options to the generic option parser,
        create your own ArgumentParser instance, use parser.add_argument and then pass your parser instance as parameter.
        Your options/params will then be available on self.options and self.args
        @param daemonize : If set to False, force the instance *not* to daemonize, even if '-f' is not passed
        on the command line. If set to True (default), will check if -f was added.
        @param log_prefix : If set, use this prefix when creating the log file in Logger()
        '''
        BasePlugin.__init__(self, name, stop_cb, parser, daemonize, log_prefix)
        Watcher(self)
        self.log.info(u"----------------------------------")
        self.log.info(u"Starting plugin '{0}' (new manager instance)".format(name))
        self.log.info(u"Python version is {0}".format(sys.version_info))
        if self.options.test_option:
            self.log.info(u"The plugin is starting in TEST mode. Test option is {0}".format(self.options.test_option))
        self._name = name
        self._test = test   # flag used to avoid loading json in test mode
        
        '''
        Calculate the MQ name
        - For a core component this is just its component name (self._name)
        - For a plugin this is plugin-<self._name>-self.hostname

        The reason is that the core components need a fixed name on the mq network,
        if a plugin starts up it needs to request the config on the network, and it needs to know the worker (core component)
        to ask the config from.

        Because of the above reason, every item in the core_component list can only run once
        '''
        if self._name in CORE_COMPONENTS:
            self._mq_name = self._name
        else:
            self._mq_name = "plugin-{0}.{1}".format(self._name, self.get_sanitized_hostname())

        # MQ publisher and REP
        self.zmq = zmq.Context()
        self._pub = MQPub(self.zmq, self._mq_name)
        self._set_status(STATUS_STARTING)

        # MQ : start the thread which sends the status each N seconds
        thr_send_status = threading.Thread(None,
                                           self._send_status_loop,
                                           "send_status_loop",
                                           (),
                                           {})
        thr_send_status.start()

        ### MQ
        # for stop requests
        MQRep.__init__(self, self.zmq, self._mq_name)

        self.helpers = {}
        self._is_manager = is_manager
        cfg = Loader('domogik')
        my_conf = cfg.load()
        self._config_files = CONFIG_FILE
        self.config = dict(my_conf[1])
 
        self.libraries_directory = self.config['libraries_path']
        self.packages_directory = "{0}/{1}".format(self.config['libraries_path'], PACKAGES_DIR)
        self.resources_directory = "{0}/{1}".format(self.config['libraries_path'], RESOURCES_DIR)
        self.products_directory = "{0}/{1}_{2}/{3}".format(self.packages_directory, "plugin", self._name, PRODUCTS_DIR)

        # plugin config
        self._plugin_config = None

        # Get pid and write it in a file
        self._pid_dir_path = self.config['pid_dir_path']
        self._get_pid()

        if len(self.get_sanitized_hostname()) > 16:
            self.log.error(u"You must use 16 char max hostnames ! {0} is {1} long".format(self.get_sanitized_hostname(), len(self.get_sanitized_hostname())))
            self.force_leave()
            return

        # Create object which get process informations (cpu, memory, etc)
        # TODO : activate
        # TODO : use something else that xPL ?????????
        #self._process_info = ProcessInfo(os.getpid(),
        #                                 TIME_BETWEEN_EACH_PROCESS_STATUS,
        #                                 self._send_process_info,
        #                                 self.log,
        #                                 self.myxpl)
        #self._process_info.start()

        self.dont_run_ready = False

        # for all no core elements, load the json
        # TODO find a way to do it nicer ??
        if self._name not in CORE_COMPONENTS and self._test == False:
            self._load_json()

        # init an empty devices list
        self.devices = []
        # init an empty 'new' devices list
        self.new_devices = []

        # check for products pictures
        if self._name not in CORE_COMPONENTS and self._test == False:
            self.check_for_pictures()

        # init finished
        self.log.info(u"End init of the global Plugin part")


    def check_configured(self):
        """ For a plugin only
            To be call in the plugin __init__()
            Check in database (over queryconfig) if the key 'configured' is set to True for the plugin
            if not, stop the plugin and log this
        """
        self._plugin_config = Query(self.zmq, self.log)
        configured = self._plugin_config.query(self._name, 'configured')
        if configured == '1':
            configured = True
        if configured != True:
            self.log.error(u"The plugin is not configured (configured = '{0}'. Stopping the plugin...".format(configured))
            self.force_leave(status = STATUS_NOT_CONFIGURED)
            return False
        self.log.info(u"The plugin is configured. Continuing (hoping that the user applied the appropriate configuration ;)")
        return True


    def _load_json(self):
        """ Load the plugin json file
        """
        try:
            self.log.info(u"Read the json file and validate id".format(self._name))
            pkg_json = PackageJson(pkg_type = "plugin", name = self._name)
            # check if json is valid
            if pkg_json.validate() == False:
                # TODO : how to get the reason ?
                self.log.error(u"Invalid json file")
                self.force_leave(status = STATUS_INVALID)
            else:
                # if valid, store the data so that it can be used later
                self.log.info(u"The json file is valid")
                self.json_data = pkg_json.get_json()
        except:
            self.log.error(u"Error while trying to read the json file : {1}".format(self._name, traceback.format_exc()))
            self.force_leave(status = STATUS_INVALID)

    def get_config(self, key):
        """ Try to get the config over the MQ. If value is None, get the default value
        """
        if self._plugin_config == None:
            self._plugin_config = Query(self.zmq, self.log)
        value = self._plugin_config.query(self._name, key)
        if value == None or value == 'None':
            self.log.info(u"Value for '{0}' is None or 'None' : trying to get the default value instead...".format(key))
            value = self.get_config_default_value(key)
        self.log.info(u"Value for '{0}' is : {1}".format(key, value))
        return self.cast_config_value(key, value)

    def get_config_default_value(self, key):
        """ Get the default value for a config key from the json file
            @param key : configuration key
        """
        for idx in range(len(self.json_data['configuration'])):
            if self.json_data['configuration'][idx]['key'] == key:
                default = self.json_data['configuration'][idx]['default']
                self.log.info(u"Default value required for key '{0}' = {1}".format(key, default))
                return default

    def cast_config_value(self, key, value):
        """ Cast the config value as the given type in the json file
            @param key : configuration key
            @param value : configuration value to cast and return
            @return : the casted value
        """
        for idx in range(len(self.json_data['configuration'])):
            if self.json_data['configuration'][idx]['key'] == key:
                type = self.json_data['configuration'][idx]['default']
                self.log.info(u"Casting value for key '{0}' in type '{1}'...".format(key, type)) 
                return self.cast(value, type)

        # no cast operation : return the value
        if value == "None":
            return None
        return value

    def cast(self, value, type):
        """ Cast a value for a type
            @param value : value to cast
            @param type : type in which you want to cast the value
        """
        try:
            if type == "boolean":
                # just in case, the "True"/"False" are not already converted in True/False
                # this is (currently) done on queryconfig side
                if value == "True":
                    return True
                elif value ==  "False":
                    return False
            # type == choice : nothing to do
            if type == "date": 
                self.log.error(u"TODO : the cast in date format is not yet developped. Please request fritz_smh to do it")
            if type == "datetime": 
                self.log.error(u"TODO : the cast in date format is not yet developped. Please request fritz_smh to do it")
            # type == email : nothing to do
            if type == "float":
                return float(value)
            if type == "integer":
                return float(value)
            # type == ipv4 : nothing to do
            # type == multiple choice : nothing to do
            # type == string : nothing to do
            if type == "time": 
                self.log.error(u"TODO : the cast in date format is not yet developped. Please request fritz_smh to do it")
            # type == url : nothing to do

        except:
            # if an error occurs : return the default value and log a warning
            self.log.warning(u"Error while casting value '{0}' to type '{1}'. The plugin may not work!! Error : {2}".format(value, type, traceback.format_exc()))
            return value
        return value

    def get_device_list(self, quit_if_no_device = False):
        """ Request the dbmgr component over MQ to get the devices list for this client
            @param quit_if_no_device: if True, exit the plugin if there is no devices
        """
        self.log.info(u"Retrieve the devices list for this client...")
        mq_client = MQSyncReq(self.zmq)
        msg = MQMessage()
        msg.set_action('device.get')
        msg.add_data('type', 'plugin')
        msg.add_data('name', self._name)
        msg.add_data('host', self.get_sanitized_hostname())
        result = mq_client.request('dbmgr', msg.get(), timeout=10)
        if not result:
            self.log.error(u"Unable to retrieve the device list")
            self.force_leave()
            return []
        else:
            device_list = result.get_data()['devices']
            if device_list == []:
                self.log.warn(u"There is no device created for this client")
                if quit_if_no_device:
                    self.log.warn(u"The developper requested to stop the client if there is no device created")
                    self.force_leave()
                    return []
            for a_device in device_list:
                self.log.info(u"- id : {0}  /  name : {1}  /  device type id : {2}".format(a_device['id'], \
                                                                                    a_device['name'], \
                                                                                    a_device['device_type_id']))
                # log some informations about the device
                # notice that even if we are not in the XplPlugin class we will display xpl related informations :
                # for some no xpl plugins, there will just be nothing to display.

                # first : the stats
                self.log.info(u"  xpl_stats features :")
                for a_xpl_stat in a_device['xpl_stats']:
                    self.log.info(u"  - {0}".format(a_xpl_stat))
                    self.log.info(u"    Static Parameters :")
                    for a_feature in a_device['xpl_stats'][a_xpl_stat]['parameters']['static']:
                        self.log.info(u"    - {0} = {1}".format(a_feature['key'], a_feature['value']))
                    self.log.info(u"    Dynamic Parameters :")
                    for a_feature in a_device['xpl_stats'][a_xpl_stat]['parameters']['dynamic']:
                        self.log.info(u"    - {0}".format(a_feature['key']))

                # then, the commands
                self.log.info(u"  xpl_commands features :")
                for a_xpl_cmd in a_device['xpl_commands']:
                    self.log.info(u" - {0}".format(a_xpl_cmd))
                    self.log.info(u" + Parameters :")
                    for a_feature in a_device['xpl_commands'][a_xpl_cmd]['parameters']:
                        self.log.info(u" - {0} = {1}".format(a_feature['key'], a_feature['value']))

            self.devices = device_list
            return device_list


    def device_detected(self, data):
        """ The plugin developpers can call this function when a device is detected
            This function will check if a corresponding device exists and :
            - if so, do nothing
            - if not, add the device in a 'new devices' list
                 - if the device is already in the 'new devices list', does nothing
                 - if not : add it into the list and send a MQ message : an event for the UI to say a new device is detected

            @param data : data about the device 
            
            Data example : 
            {
                "device_type" : "...",
                "reference" : "...",
                "global" : [
                    { 
                        "key" : "....",
                        "value" : "...."
                    },
                    ...
                ],
                "xpl" : [
                    { 
                        "key" : "....",
                        "value" : "...."
                    },
                    ...
                ],
                "xpl_commands" : {
                    "command_id" : [
                        { 
                            "key" : "....",
                            "value" : "...."
                        },
                        ...
                    ],
                    "command_id_2" : [...]
                },
                "xpl_stats" : {
                    "sensor_id" : [
                        { 
                            "key" : "....",
                            "value" : "...."
                        },
                        ...
                    ],
                    "sensor_id_2" : [...]
                }
            }
        """
        self.log.debug(u"Device detected : data = {0}".format(data))
        # browse all devices to find if the device exists
        found = False
        for a_device in self.devices:
            # TODO : set the id if the device exists!
            pass

        if found:
            self.log.debug(u"The device already exists : id={0}.".format(a_device['id']))
        else:
            self.log.debug(u"The device doesn't exists in database")
            # generate a unique id for the device from its addresses
            new_device_id = self.generate_detected_device_id(data)
         
            # add the device feature in the new devices list : self.new_devices[device_type][type][feature] = data
            self.log.debug(u"Check if the device has already be marked as new...")
            found = False
            for a_device in self.new_devices:
                if a_device['id'] == new_device_id:
                    found = True

            #for a_device in self.new_devices:
            #    if a_device['device_type_id'] == device_type and \
            #       a_device['type'] == type and \
            #       a_device['feature'] == feature:
#
            #       if data == a_device['data']:
            #            found = True
                    
            if found == False:
                new_device = {'id' : new_device_id, 'data' : data}
                self.log.info(u"New device feature detected and added in the new devices list : {0}".format(new_device))
                self.new_devices.append(new_device)

                # publish new devices update
                self._pub.send_event('device.new',
                                     {"type" : "plugin",
                                      "name" : self._name,
                                      "host" : self.get_sanitized_hostname(),
                                      "client_id" : "plugin-{0}.{1}".format(self._name, self.get_sanitized_hostname()),
                                      "device" : new_device})

                # TODO : later (0.4.0+), publish one "new device" notification with only the new device detected

            else:
                self.log.debug(u"The device has already been detected since the plugin startup")

    def generate_detected_device_id(self, data):
        """ Generate an unique id based on the content of data
        """
        # TODO : improve to make something more sexy ?
        the_id = json.dumps(data, sort_keys=True) 
        chars_to_remove = ['"', '{', '}', ',', ' ', '=', '[', ']', ':']
        the_id = the_id.translate(None, ''.join(chars_to_remove))
        return the_id


    def OLD_device_detected(self, device_type, type, feature, data):
        """ The plugin developpers can call this function when a device is detected
            This function will check if a corresponding device exists and : 
            - if so, do nothing
            - if not, add the device in a 'new devices' list
                 - if the device is already in the 'new devices list', does nothing
                 - if not : add it into the list and send a MQ message : an event for the UI to say a new device is detected

            ### TODO : implement a req/rep MQ message to allow UI to get the new devices list

            @param device_type : device_type of the detected device
            @param data : data about the device (address or any other configuration element of a device for this plugin)
            @param type : xpl_stats, xpl_commands
            @param feature : a xpl_stat or xpl_command feature
        """
        self.log.debug(u"Device detected : device_type = {0}, data = {1}".format(device_type, data))
        #self.log.debug(u"Already existing devices : {0}".format(self.devices))
        # browse all devices to find if the device exists
        found = False
        for a_device in self.devices:
            # first, search for device type
            if a_device['device_type_id'] == device_type:
                params = a_device[type][feature]['parameters']['static']
                found = True
                for key in data:
                    for a_param in params:
                        if key == a_param['key'] and data[key] != a_param['value']:
                            found = False
                            break
                if found:
                    break
        if found:
            self.log.debug(u"The device already exists : id={0}.".format(a_device['id']))
        else:
            self.log.debug(u"The device doesn't exists in database")
         
            # add the device feature in the new devices list : self.new_devices[device_type][type][feature] = data
            self.log.debug(u"Check if the device has already be marked as new...")
            found = False
            for a_device in self.new_devices:
                if a_device['device_type_id'] == device_type and \
                   a_device['type'] == type and \
                   a_device['feature'] == feature:

                   if data == a_device['data']:
                        found = True
                    
            if found == False:
                new_device ={'device_type_id' : device_type,
                             'type' : type,
                             'feature' : feature,
                             'data' : data}
                self.log.info(u"New device feature detected and added in the new devices list : {0}".format(new_device))
                self.new_devices.append(new_device)

                # publish new devices update
                self._pub.send_event('device.new',
                                     {"type" : "plugin",
                                      "name" : self._name,
                                      "host" : self.get_sanitized_hostname(),
                                      "client_id" : "plugin-{0}.{1}".format(self._name, self.get_sanitized_hostname()),
                                      "device" : new_device})

                # TODO : later (0.4.0+), publish one "new device" notification with only the new device detected

            else:
                self.log.debug(u"The device has already been detected since the plugin startup")


    def get_parameter(self, a_device, key):
        """ For a device feature, return the required parameter value
            @param a_device: the device informations
            @param key: the parameter key
        """
        try:
            self.log.debug(u"Get parameter '{0}'".format(key))
            for a_param in a_device['parameters']:
                if a_param == key:
                    value = self.cast(a_device['parameters'][a_param]['value'], a_device['parameters'][a_param]['type'])
                    self.log.debug(u"Parameter value found: {0}".format(value))
                    return value
            self.log.warning(u"Parameter not found : return None")
            return None
        except:
            self.log.error(u"Error while looking for a device parameter. Return None. Error: {0}".format(traceback.format_exc()))
            return None
         

    def get_parameter_for_feature(self, a_device, type, feature, key):
        """ For a device feature, return the required parameter value
            @param a_device: the device informations
            @param type: the parameter type (xpl_stats, ...)
            @param feature: the parameter feature
            @param key: the parameter key
        """
        try:
            self.log.debug(u"Get parameter '{0}' for '{1}', feature '{2}'".format(key, type, feature))
            for a_param in a_device[type][feature]['parameters']['static']:
                if a_param['key'] == key:
                    value = self.cast(a_param['value'], a_param['type'])
                    self.log.debug(u"Parameter value found: {0}".format(value))
                    return value
            self.log.warning(u"Parameter not found : return None")
            return None
        except:
            self.log.error(u"Error while looking for a device feature parameter. Return None. Error: {0}".format(traceback.format_exc()))
            return None
         

    def check_for_pictures(self):
        """ if some products are defined, check if the corresponding pictures are present in the products/ folder
        """
        self.log.info(u"Check if there are pictures for the defined products")
        ok = True
        ok_product = None
        if self.json_data.has_key('products'):
            for product in self.json_data['products']:
                ok_product = False
                for ext in PRODUCTS_PICTURES_EXTENSIONS:
                    file = "{0}.{1}".format(product['id'], ext)
                    if os.path.isfile("{0}/{1}".format(self.get_products_directory(), file)):
                        ok_product = True
                        break
                if ok_product:
                    self.log.debug(u"- OK : {0} ({1})".format(product['name'], file))
                else:
                    ok = False
                    self.log.warning(u"- Missing : {0} ({1}.{2})".format(product['name'], product['id'], PRODUCTS_PICTURES_EXTENSIONS))
        if ok == False:
            self.log.warning(u"Some pictures are missing!")
        else:
            if ok_product == None:
                self.log.info(u"There is no products defined for this plugin")


    def ready(self, ioloopstart=1):
        """ to call at the end of the __init__ of classes that inherits of this one
 
            In the XplPLugin class, this function will be completed to also activate the xpl hbeat
        """
        if self.dont_run_ready == True:
            return

        ### send plugin status : STATUS_ALIVE
        # TODO : why the dbmgr has no self._name defined ???????
        # temporary set as unknown to avoir blocking bugs
        if not hasattr(self, '_name'):
            self._name = "unknown"
        self._set_status(STATUS_ALIVE)

        ### Instantiate the MQ
        # nothing can be launched after this line (blocking call!!!!)
        self.log.info(u"Start IOLoop for MQ : nothing else can be executed in the __init__ after this! Make sure that the self.ready() call is the last line of your init!!!!")
        if ioloopstart == 1:
            IOLoop.instance().start()



    def on_mdp_request(self, msg):
        """ Handle Requests over MQ
            @param msg : MQ req message
        """
        self.log.debug(u"MQ Request received : {0}" . format(str(msg)))

        ### stop the plugin
        if msg.get_action() == "plugin.stop.do":
            self.log.info(u"Plugin stop request : {0}".format(msg))
            self._mdp_reply_plugin_stop(msg)
        elif msg.get_action() == "helper.list.get":
            self.log.info(u"Plugin helper list request : {0}".format(msg))
            self._mdp_reply_helper_list(msg)
        elif msg.get_action() == "helper.help.get":
            self.log.info(u"Plugin helper help request : {0}".format(msg))
            self._mdp_reply_helper_help(msg)
        elif msg.get_action() == "helper.do":
            self.log.info(u"Plugin helper action request : {0}".format(msg))
            self._mdp_reply_helper_do(msg)
        elif msg.get_action() == "device.new.get":
            self.log.info(u"Plugin new devices request : {0}".format(msg))
            self._mdp_reply_device_new_get(msg)
    
    def _mdp_reply_helper_do(self, msg):
        contens = msg.get_data()
        if 'command' in contens.keys():
            if contens['command'] in self.helpers.keys():
                if 'parameters' not in contens.keys():
                    contens['parameters'] = {}
                    params = []
                else:
                    params = []
                    for key, value in contens['parameters'].items():
                        params.append( "{0}='{1}'".format(key, value) )
                command = "self.{0}(".format(self.helpers[contens['command']]['call'])
                command += ", ".join(params)
                command += ")"
                result = eval(command)
                # run the command with all params
                msg = MQMessage()
                msg.set_action('helper.do.result')
                msg.add_data('command', contens['command'])
                msg.add_data('parameters', contens['parameters'])
                msg.add_data('result', result)
                self.reply(msg.get())

    def _mdp_reply_helper_help(self, data):
        content = data.get_data()
        if 'command' in contens.keys():
            if content['command'] in self.helpers.keys():
                msg = MQMessage()
                msg.set_action('helper.help.result')
                msg.add_data('help', self.helpers[content['command']]['help'])
                self.reply(msg.get())

    def _mdp_reply_plugin_stop(self, data):
        """ Stop the plugin
            @param data : MQ req message

            First, send the MQ Rep to 'ack' the request
            Then, change the plugin status to STATUS_STOP_REQUEST
            Then, quit the plugin by calling force_leave(). This should make the plugin send a STATUS_STOPPED if all is ok

            Notice that no check is done on the MQ req content : we need nothing in it as it is directly addressed to a plugin
        """
        # check if the message is for us
        content = data.get_data()
        if content['name'] != self._name or content['host'] != self.get_sanitized_hostname():
            return

        ### Send the ack over MQ Rep
        msg = MQMessage()
        msg.set_action('plugin.stop.result')
        status = True
        reason = ""
        msg.add_data('status', status)
        msg.add_data('reason', reason)
        msg.add_data('name', self._name)
        msg.add_data('host', self.get_sanitized_hostname())
        self.log.info("Send reply for the stop request : {0}".format(msg))
        self.reply(msg.get())

        ### Change the plugin status
        self._set_status(STATUS_STOP_REQUEST)

        ### Try to stop the plugin
        # if it fails, the manager should try to kill the plugin
        self.force_leave()

    def _mdp_reply_helper_list(self, data):
        """ Return a list of supported helpers
            @param data : MQ req message
        """
        ### Send the ack over MQ Rep
        msg = MQMessage()
        msg.set_action('helper.list.result')
        msg.add_data('actions', self.helpers.keys())
        self.reply(msg.get())

    def _mdp_reply_device_new_get(self, data):
        """ Return a list of new devices detected
            @param data : MQ req message
        """
        ### Send the ack over MQ Rep
        msg = MQMessage()
        msg.set_action('device.new.result')
        msg.add_data('devices', self.new_devices)
        self.reply(msg.get())


    def _set_status(self, status):
        """ Set the plugin status and send it
        """
        # when ctrl-c is done, there is no more self._name at this point...
        # why ? because the force_leave method is called twice as show in the logs : 
        #
        # ^CKeyBoardInterrupt
        # 2013-12-20 22:48:41,040 domogik-manager INFO Keyoard Interrupt detected, leave now.
        # Traceback (most recent call last):
        #   File "./manager.py", line 1176, in <module>
        #     main()
        #   File "./manager.py", line 1173, in main
        # 2013-12-20 22:48:41,041 domogik-manager DEBUG force_leave called
        # 2013-12-20 22:48:41,044 domogik-manager DEBUG __del__ Single xpl plugin
        # 2013-12-20 22:48:41,045 domogik-manager DEBUG force_leave called

        if hasattr(self, '_name'):
            if self._name not in CORE_COMPONENTS:
                self._status = status
                self._send_status()

    def _send_status_loop(self):
        """ send the status each STATUS_HBEAT seconds
        """
        # TODO : we could optimize by resetting the timer each time the status is sent
        # but as this is used only to check for dead plugins by the manager, it is not very important ;)
        while not self._stop.isSet():
            self._send_status()
            self._stop.wait(STATUS_HBEAT)

    def _send_status(self):
        """ Send the plugin status over the MQ
        """ 
        if hasattr(self, "_pub"):
            if self._name in CORE_COMPONENTS:
                #type = "core"
                return
            else:
                type = "plugin"
            self.log.debug("Send plugin status : {0}".format(self._status))
            self._pub.send_event('plugin.status', 
                                 {"type" : type,
                                  "name" : self._name,
                                  "host" : self.get_sanitized_hostname(),
                                  "event" : self._status})

    def get_config_files(self):
       """ Return list of config files
       """
       return self._config_files

    def get_products_directory(self):
       """ getter 
       """
       return self.products_directory

    def get_libraries_directory(self):
       """ getter 
       """
       return self.libraries_directory

    def get_packages_directory(self):
       """ getter 
       """
       return self.packages_directory

    def get_resources_directory(self):
       """ getter 
       """
       return self.resources_directory

    def get_data_files_directory(self):
       """
       Return the directory where a plugin developper can store data files.
       If the directory doesn't exist, try to create it.
       After that, try to create a file inside it.
       If something goes wrong, generate an explicit exception.
       """
       path = "{0}/{1}/{2}_{3}/data/".format(self.libraries_directory, PACKAGES_DIR, "plugin", self._name)
       if os.path.exists(path):
           if not os.access(path, os.W_OK & os.X_OK):
               raise OSError("Can't write in directory {0}".format(path))
       else:
           try:
               os.mkdir(path, '0770')
               self.log.info(u"Create directory {0}.".format(path))
           except:
               raise OSError("Can't create directory {0}.".format(path))
       # Commented because :
       # a write test is done for each call of this function. For a plugin with a html server (geoloc for example), it
       # can be an issue as this makes a lot of write for 'nothing' on the disk.
       # We keep the code for now (0.4) for maybe a later use (and improved)
       #try:
       #    tmp_prefix = "write_test";
       #    count = 0
       #    filename = os.path.join(path, tmp_prefix)
       #    while(os.path.exists(filename)):
       #        filename = "{}.{}".format(os.path.join(path, tmp_prefix),count)
       #        count = count + 1
       #    f = open(filename,"w")
       #    f.close()
       #    os.remove(filename)
       #except :
       #    raise IOError("Can't create a file in directory {0}.".format(path))
       return path

    def register_helper(self, action, help_string, callback):
        if action not in self.helpers:
            self.helpers[action] = {'call': callback, 'help': help_string}

    def publish_helper(self, key, data):
        if hasattr(self, "_pub"):
            if self._name in CORE_COMPONENTS:
                type = "core"
            else:
                type = "plugin"
            self._pub.send_event('helper.publish',
                                 {"origin" : self._mq_name,
                                  "key": key,
                                  "data": data})

    def _get_pid(self):
        """ Get current pid and write it to a file
        """
        pid = os.getpid()
        pid_file = os.path.join(self._pid_dir_path,
                                self._name + ".pid")
        self.log.debug(u"Write pid file for pid '{0}' in file '{1}'".format(str(pid), pid_file))
        fil = open(pid_file, "w")
        fil.write(str(pid))
        fil.close()

    def __del__(self):
        if hasattr(self, "log"):
            self.log.debug(u"__del__ Single plugin")
            self.log.debug(u"the stack is :")
            for elt in inspect.stack():
                self.log.debug(u"    {0}".format(elt))
            # we guess that if no "log" is defined, the plugin has not really started, so there is no need to call force leave (and _stop, .... won't be created)
            self.force_leave()

    def force_leave(self, status = False, return_code = None):
        """ Leave threads & timers

            In the XplPLugin class, this function will be completed to also activate the xpl hbeat
        """
        if hasattr(self, "log"):
            self.log.debug(u"force_leave called")
            #self.log.debug(u"the stack is : {0}".format(inspect.stack()))
            self.log.debug(u"the stack is :")
            for elt in inspect.stack():
                self.log.debug(u"    {0}".format(elt))

        if return_code != None:
            self.set_return_code(return_code)
            self.log.info("Return code set to {0} when calling force_leave()".format(return_code))


        # avoid ready() to be launched
        self.dont_run_ready = True
        # stop IOLoop
        #try:
        #    IOLoop.instance().start()
        #except:
        #    pass
        # send stopped status over the MQ
        if status:
            self._set_status(status)
        else:
            self._set_status(STATUS_STOPPED)

        # try to stop the thread
        try:
            self.get_stop().set()
        except AttributeError:
            pass

        if hasattr(self, "_timers"):
            for t in self._timers:
                if hasattr(self, "log"):
                    self.log.debug(u"Try to stop timer {0}".format(t))
                t.stop()
                if hasattr(self, "log"):
                    self.log.debug(u"Timer stopped {0}".format(t))

        if hasattr(self, "_stop_cb"):
            for cb in self._stop_cb:
                if hasattr(self, "log"):
                    self.log.debug(u"Calling stop additionnal method : {0} ".format(cb.__name__))
                cb()
    
        if hasattr(self, "_threads"):
            for t in self._threads:
                if hasattr(self, "log"):
                    self.log.debug(u"Try to stop thread {0}".format(t))
                try:
                    t.join()
                except RuntimeError:
                    pass
                if hasattr(self, "log"):
                    self.log.debug(u"Thread stopped {0}".format(t))
                #t._Thread__stop()

        #Finally, we try to delete all remaining threads
        for t in threading.enumerate():
            if t != threading.current_thread() and t.__class__ != threading._MainThread:
                if hasattr(self, "log"):
                    self.log.info(u"The thread {0} was not registered, killing it".format(t.name))
                t.join()
                if hasattr(self, "log"):
                    self.log.info(u"Thread {0} stopped.".format(t.name))

        if threading.activeCount() > 1:
            if hasattr(self, "log"):
                self.log.warn(u"There are more than 1 thread remaining : {0}".format(threading.enumerate()))
Beispiel #6
0
    def __init__(self, name, host, clients, libraries_directory, packages_directory, zmq_context, stop, local_host):
        """ Init a plugin 
            @param name : plugin name (ipx800, onewire, ...)
            @param host : hostname
            @param clients : clients list 
            @param libraries_directory : path for the base python module for packages : /var/lib/domogik/
            @param packages_directory : path in which are stored the packages : /var/lib/domogik/packages/
            @param zmq_context : zmq context
            @param stop : get_stop()
            @param local_host : get_sanitized_hostname()
        """
        GenericComponent.__init__(self, name = name, host = host, clients = clients)
        self.log.info(u"New plugin : {0}".format(self.name))

        ### check if the plugin is on he local host
        if self.host == local_host:
            self.local_plugin = True
        else:
            self.local_plugin = False

        ### TODO : this will be to handle later : multi host (multihost)
        # * how to find available plugins on other hosts ?
        # * how to start/stop plugins on other hosts ?
        # * ...
        if self.local_plugin == False:
            self.log.error(u"Currently, the multi host feature for plugins is not yet developped. This plugin will not be registered")
            return

        ### set the component type
        self.type = "plugin"

        ### set package path
        self._packages_directory = packages_directory
        self._libraries_directory = libraries_directory

        ### zmq context
        self.zmq = zmq_context

        ### XplPlugin
        self._stop = stop

        ### config
        # used only in the function add_configuration_values_to_data()
        # elsewhere, this function is used : self.get_config("xxxx")
        self._config = Query(self.zmq, self.log)

        ### get the plugin data (from the json file)
        status = None
        self.data = {}
        self.fill_data()

        ### check if the plugin is configured (get key 'configured' in database over queryconfig)
        configured = self._config.query(self.name, 'configured')
        if configured == '1':
            configured = True
        if configured == True:
            #self.set_configured(True)
            self.configured = True
        else:
            #self.set_configured(False)
            self.configured = False

        ### register the plugin as a client
        self.register_component()

        ### subscribe the the MQ for category = plugin and name = self.name
        MQAsyncSub.__init__(self, self.zmq, 'manager', ['plugin.status', 'plugin.configuration'])

        ### check if the plugin must be started on manager startup
        startup = self._config.query(self.name, 'auto_startup')
        if startup == '1':
            startup = True
        if startup == True:
            self.log.info(u"Plugin {0} configured to be started on manager startup. Starting...".format(name))
            pid = self.start()
            if pid:
                self.log.info(u"Plugin {0} started".format(name))
            else:
                self.log.error(u"Plugin {0} failed to start".format(name))
        else:
            self.log.info(u"Plugin {0} not configured to be started on manager startup.".format(name))
Beispiel #7
0
class Plugin(GenericComponent, MQAsyncSub):
    """ This helps to handle plugins discovered on the host filesystem
        The MQAsyncSub helps to set the status 

        Notice that some actions can't be done if the plugin host is not the server host! :
        * check if a plugin has stopped and kill it if needed
        * start the plugin
    """

    def __init__(self, name, host, clients, libraries_directory, packages_directory, zmq_context, stop, local_host):
        """ Init a plugin 
            @param name : plugin name (ipx800, onewire, ...)
            @param host : hostname
            @param clients : clients list 
            @param libraries_directory : path for the base python module for packages : /var/lib/domogik/
            @param packages_directory : path in which are stored the packages : /var/lib/domogik/packages/
            @param zmq_context : zmq context
            @param stop : get_stop()
            @param local_host : get_sanitized_hostname()
        """
        GenericComponent.__init__(self, name = name, host = host, clients = clients)
        self.log.info(u"New plugin : {0}".format(self.name))

        ### check if the plugin is on he local host
        if self.host == local_host:
            self.local_plugin = True
        else:
            self.local_plugin = False

        ### TODO : this will be to handle later : multi host (multihost)
        # * how to find available plugins on other hosts ?
        # * how to start/stop plugins on other hosts ?
        # * ...
        if self.local_plugin == False:
            self.log.error(u"Currently, the multi host feature for plugins is not yet developped. This plugin will not be registered")
            return

        ### set the component type
        self.type = "plugin"

        ### set package path
        self._packages_directory = packages_directory
        self._libraries_directory = libraries_directory

        ### zmq context
        self.zmq = zmq_context

        ### XplPlugin
        self._stop = stop

        ### config
        # used only in the function add_configuration_values_to_data()
        # elsewhere, this function is used : self.get_config("xxxx")
        self._config = Query(self.zmq, self.log)

        ### get the plugin data (from the json file)
        status = None
        self.data = {}
        self.fill_data()

        ### check if the plugin is configured (get key 'configured' in database over queryconfig)
        configured = self._config.query(self.name, 'configured')
        if configured == '1':
            configured = True
        if configured == True:
            #self.set_configured(True)
            self.configured = True
        else:
            #self.set_configured(False)
            self.configured = False

        ### register the plugin as a client
        self.register_component()

        ### subscribe the the MQ for category = plugin and name = self.name
        MQAsyncSub.__init__(self, self.zmq, 'manager', ['plugin.status', 'plugin.configuration'])

        ### check if the plugin must be started on manager startup
        startup = self._config.query(self.name, 'auto_startup')
        if startup == '1':
            startup = True
        if startup == True:
            self.log.info(u"Plugin {0} configured to be started on manager startup. Starting...".format(name))
            pid = self.start()
            if pid:
                self.log.info(u"Plugin {0} started".format(name))
            else:
                self.log.error(u"Plugin {0} failed to start".format(name))
        else:
            self.log.info(u"Plugin {0} not configured to be started on manager startup.".format(name))


    def on_message(self, msgid, content):
        """ when a message is received from the MQ 
        """
        #self.log.debug(u"New pub message received {0}".format(msgid))
        #self.log.debug(u"{0}".format(content))
        if msgid == "plugin.status":
            if content["name"] == self.name and content["host"] == self.host:
                self.log.info(u"New status received from {0} on {1} : {2}".format(self.name, self.host, content["event"]))
                self.set_status(content["event"])
                # if the status is STATUS_STOP_REQUEST, launch a check in N seconds to check if the plugin was able to shut alone
                if content["event"] == STATUS_STOP_REQUEST:
                    self.log.info(u"The plugin '{0}' is requested to stop. In 15 seconds, we will check if it has stopped".format(self.name))
                    thr_check_if_stopped = Thread(None,
                                                  self._check_if_stopped,
                                                  "check_if_{0}_is_stopped".format(self.name),
                                                  (),
                                                  {})
                    thr_check_if_stopped.start()
        elif msgid == "plugin.configuration":
            self.add_configuration_values_to_data()
            self._clients.publish_update()


    def reload_data(self):
        """ Just reload the client data
        """
        self.data = {}
        self.fill_data()

    def fill_data(self):
        """ Fill the client data by reading the json file
        """
        try:
            self.log.info(u"Plugin {0} : read the json file".format(self.name))
            pkg_json = PackageJson(pkg_type = "plugin", name = self.name)
            #we don't need to validate the json file as it has already be done in the check_avaiable_packages function
            self.data = pkg_json.get_json()
            self.add_configuration_values_to_data()
        except PackageException as e:
            self.log.error(u"Plugin {0} : error while trying to read the json file".format(self.name))
            self.log.error(u"Plugin {0} : invalid json file".format(self.name))
            self.log.error(u"Plugin {0} : {1}".format(self.name, e.value))
            self.set_status(STATUS_INVALID)
            pass

    def add_configuration_values_to_data(self):
        """
        """
        # grab all the config elements for the plugin
        config = self._config.query(self.name)
        if config != None:
            for key in config:
                # filter on the 'configured' key
                if key == 'configured':
                    self.configured = True
                    self.set_configured(True)
                else:
                    # check if the key exists in the plugin configuration
                    key_found = False
                    # search the key in the configuration json part
                    for idx in range(len(self.data['configuration'])):
                        if self.data['configuration'][idx]['key'] == key:
                            key_found = True
                            # key found : insert value in the json
                            self.data['configuration'][idx]['value'] = config[key]
                    if key_found == False:
                        self.log.warning(u"A key '{0}' is configured for plugin {1} on host {2} but there is no such key in the json file of the plugin. You may need to clean your configuration".format(key, self.name, self.host))

    def start(self):
        """ to call to start the plugin
            @return : None if ko
                      the pid if ok
        """
        ### Check if the plugin is not already launched
        # notice that this test is not really needed as the plugin also test this in startup...
        # but the plugin does it before the MQ is initiated, so the error message won't go overt the MQ.
        # By doing it now, the error will go to the UI through the 'error' MQ messages (sended by self.log.error)
        res, pid_list = is_already_launched(self.log, self.name)
        if res:
            return 0

        ### Actions for test mode
        test_mode = self._config.query(self.name, "test_mode")
        test_option = self._config.query(self.name, "test_option")
        test_args = ""
        if test_mode == True: 
            self.log.info("The plugin {0} is requested to be launched in TEST mode. Option is {1}".format(self.name, test_option))
            test_args = "-T {0}".format(test_option)

        ### Try to start the plugin
        self.log.info(u"Request to start plugin : {0} {1}".format(self.name, test_args))
        pid = self.exec_component(py_file = "{0}/plugin_{1}/bin/{2}.py {3}".format(self._packages_directory, self.name, self.name, test_args), \
                                  env_pythonpath = self._libraries_directory)
        pid = pid

        # There is no need to check if it is successfully started as the plugin will send over the MQ its status the UI will get the information in this way

        self.set_pid(pid)
        return pid


    def exec_component(self, py_file, env_pythonpath = None):
        """ to call to start a component
            @param py_file : path to the .py file
            @param env_pythonpath (optionnal): custom PYTHONPATH if needed (for packages it is needed)
        """
        ### Generate command
        # we add the STARTED_BY_MANAGER useless command to allow the plugin to ignore this command line when it checks if it is already laucnehd or not
        cmd = "{0} && ".format(STARTED_BY_MANAGER)
        if env_pythonpath:
            cmd += "export PYTHONPATH={0} && ".format(env_pythonpath)
        cmd += "{0} {1}".format(PYTHON, py_file)
 
        ### Execute command
        self.log.info(u"Execute command : {0}".format(cmd))
        subp = Popen(cmd, 
                     shell=True)
        pid = subp.pid
        subp.communicate()
        return pid


    def _check_if_stopped(self):
        """ Check if the plugin is stopped. If not, kill it
        """
        self._stop.wait(WAIT_AFTER_STOP_REQUEST)
        self.log.debug("Check if the plugin {0} has stopped it self. Else there will be a bloodbath".format(self.name))
        res, pid_list = is_already_launched(self.log, self.name)
        if res:
            for the_pid in pid_list:
                self.log.info(u"Try to kill pid {0}...".format(the_pid))
                os.kill(int(the_pid), signal.SIGKILL)
                # TODO : add one more check ?
                # do a while loop over is_already.... ?
            self.log.info(u"The plugin {0} should be killed now (kill -9)".format(self.name))
        else:
            self.log.info(u"The plugin {0} has stopped itself properly.".format(self.name))
Beispiel #8
0
class XplPlugin(BasePlugin, MQRep):
    '''
    Global plugin class, manage signal handlers.
    This class shouldn't be used as-it but should be extended by xPL plugin
    This class is a Singleton
    '''
    def __init__(self, name, stop_cb = None, is_manager = False, reload_cb = None, dump_cb = None, parser = None,
                 daemonize = True, nohub = False, test = False):
        '''
        Create XplPlugin instance, which defines system handlers
        @param name : The name of the current plugin
        @param stop_cb : Additionnal method to call when a stop request is received
        @param is_manager : Must be True if the child script is a Domogik Manager process
        You should never need to set it to True unless you develop your own manager
        @param reload_cb : Callback to call when a "RELOAD" order is received, if None,
        nothing will happen
        @param dump_cb : Callback to call when a "DUMP" order is received, if None,
        nothing will happen
        @param parser : An instance of ArgumentParser. If you want to add extra options to the generic option parser,
        create your own ArgumentParser instance, use parser.add_argument and then pass your parser instance as parameter.
        Your options/params will then be available on self.options and self.args
        @param daemonize : If set to False, force the instance *not* to daemonize, even if '-f' is not passed
        on the command line. If set to True (default), will check if -f was added.
        @param nohub : if set the hub discovery will be disabled
        '''
        BasePlugin.__init__(self, name, stop_cb, parser, daemonize)
        Watcher(self)
        self.log.info(u"----------------------------------")
        self.log.info(u"Starting plugin '%s' (new manager instance)" % name)
        self.log.info(u"Python version is {0}".format(sys.version_info))
        if self.options.test_option:
            self.log.info(u"The plugin is starting in TEST mode. Test option is {0}".format(self.options.test_option))
        self._name = name
        self._test = test   # flag used to avoid loading json in test mode
        
        '''
        Calculate the MQ name
        - For a core component this is just its component name (self._name)
        - For a plugin this is plugin-<self._name>-self.hostname

        The reason is that the core components need a fixed name on the mq network,
        if a plugin starts up it needs to request the config on the network, and it needs to know the worker (core component)
        to ask the config from.

        Because of the above reason, every item in the core_component list can only run once
        '''
        if self._name in CORE_COMPONENTS:
            self._mq_name = self._name
        else:
            self._mq_name = "plugin-{0}.{1}".format(self._name, self.get_sanitized_hostname())

        # MQ publisher and REP
        self.zmq = zmq.Context()
        self._pub = MQPub(self.zmq, self._mq_name)
        self._set_status(STATUS_STARTING)

        # MQ : start the thread which sends the status each N seconds
        thr_send_status = threading.Thread(None,
                                           self._send_status_loop,
                                           "send_status_loop",
                                           (),
                                           {})
        thr_send_status.start()

        ### MQ
        # for stop requests
        MQRep.__init__(self, self.zmq, self._mq_name)

        self.helpers = {}
        self._is_manager = is_manager
        cfg = Loader('domogik')
        my_conf = cfg.load()
        self._config_files = CONFIG_FILE
        config = dict(my_conf[1])
 
        self.libraries_directory = config['libraries_path']
        self.packages_directory = "{0}/{1}".format(config['libraries_path'], PACKAGES_DIR)
        self.resources_directory = "{0}/{1}".format(config['libraries_path'], RESOURCES_DIR)
        self.products_directory = "{0}/{1}_{2}/{3}".format(self.packages_directory, "plugin", self._name, PRODUCTS_DIR)

        # Get pid and write it in a file
        self._pid_dir_path = config['pid_dir_path']
        self._get_pid()

        if len(self.get_sanitized_hostname()) > 16:
            self.log.error(u"You must use 16 char max hostnames ! %s is %s long" % (self.get_sanitized_hostname(), len(self.get_sanitized_hostname())))
            self.force_leave()
            return

        if 'broadcast' in config:
            broadcast = config['broadcast']
        else:
            broadcast = "255.255.255.255"
        if 'bind_interface' in config:
            self.myxpl = Manager(config['bind_interface'], broadcast = broadcast, plugin = self, nohub = nohub)
        else:
            self.myxpl = Manager(broadcast = broadcast, plugin = self, nohub = nohub)

        self._reload_cb = reload_cb
        self._dump_cb = dump_cb

        # Create object which get process informations (cpu, memory, etc)
        # TODO : activate
        #self._process_info = ProcessInfo(os.getpid(),
        #                                 TIME_BETWEEN_EACH_PROCESS_STATUS,
        #                                 self._send_process_info,
        #                                 self.log,
        #                                 self.myxpl)
        #self._process_info.start()

        self.enable_hbeat_called = False
        self.dont_run_ready = False

        # for all no core elements, load the json
        # TODO find a way to do it nicer ??
        if self._name not in CORE_COMPONENTS and self._test == False:
            self._load_json()

        # init an empty devices list
        self.devices = []
        # init an empty 'new' devices list
        self.new_devices = []

        # check for products pictures
        if self._name not in CORE_COMPONENTS and self._test == False:
            self.check_for_pictures()

        # init finished
        self.log.debug(u"end single xpl plugin")


    def check_configured(self):
        """ For a plugin only
            To be call in the plugin __init__()
            Check in database (over queryconfig) if the key 'configured' is set to True for the plugin
            if not, stop the plugin and log this
        """
        self._config = Query(self.zmq, self.log)
        configured = self._config.query(self._name, 'configured')
        if configured == '1':
            configured = True
        if configured != True:
            self.log.error(u"The plugin is not configured (configured = '{0}'. Stopping the plugin...".format(configured))
            self.force_leave(status = STATUS_NOT_CONFIGURED)
            return False
        self.log.info(u"The plugin is configured. Continuing (hoping that the user applied the appropriate configuration ;)")
        return True


    def _load_json(self):
        """ Load the plugin json file
        """
        try:
            self.log.info(u"Read the json file and validate id".format(self._name))
            pkg_json = PackageJson(pkg_type = "plugin", name = self._name)
            # check if json is valid
            if pkg_json.validate() == False:
                # TODO : how to get the reason ?
                self.log.error(u"Invalid json file")
                self.force_leave(status = STATUS_INVALID)
            else:
                # if valid, store the data so that it can be used later
                self.log.info(u"The json file is valid")
                self.json_data = pkg_json.get_json()
        except:
            self.log.error(u"Error while trying to read the json file : {1}".format(self._name, traceback.format_exc()))
            self.force_leave(status = STATUS_INVALID)

    def get_config(self, key):
        """ Try to get the config over the MQ. If value is None, get the default value
        """
        value = self._config.query(self._name, key)
        if value == None or value == 'None':
            self.log.info(u"Value for '{0}' is None or 'None' : trying to get the default value instead...".format(key))
            value = self.get_config_default_value(key)
        self.log.info(u"Value for '{0}' is : {1}".format(key, value))
        return self.cast_config_value(key, value)

    def get_config_default_value(self, key):
        """ Get the default value for a config key from the json file
            @param key : configuration key
        """
        for idx in range(len(self.json_data['configuration'])):
            if self.json_data['configuration'][idx]['key'] == key:
                default = self.json_data['configuration'][idx]['default']
                self.log.info(u"Default value required for key '{0}' = {1}".format(key, default))
                return default

    def cast_config_value(self, key, value):
        """ Cast the config value as the given type in the json file
            @param key : configuration key
            @param value : configuration value to cast and return
            @return : the casted value
        """
        for idx in range(len(self.json_data['configuration'])):
            if self.json_data['configuration'][idx]['key'] == key:
                type = self.json_data['configuration'][idx]['default']
                self.log.info(u"Casting value for key '{0}' in type '{1}'...".format(key, type)) 
                return self.cast(value, type)

        # no cast operation : return the value
        if value == "None":
            return None
        return value

    def cast(self, value, type):
        """ Cast a value for a type
            @param value : value to cast
            @param type : type in which you want to cast the value
        """
        try:
            if type == "boolean":
                # just in case, the "True"/"False" are not already converted in True/False
                # this is (currently) done on queryconfig side
                if value == "True":
                    return True
                elif value ==  "False":
                    return False
            # type == choice : nothing to do
            if type == "date": 
                self.log.error(u"TODO : the cast in date format is not yet developped. Please request fritz_smh to do it")
            if type == "datetime": 
                self.log.error(u"TODO : the cast in date format is not yet developped. Please request fritz_smh to do it")
            # type == email : nothing to do
            if type == "float":
                return float(value)
            if type == "integer":
                return float(value)
            # type == ipv4 : nothing to do
            # type == multiple choice : nothing to do
            # type == string : nothing to do
            if type == "time": 
                self.log.error(u"TODO : the cast in date format is not yet developped. Please request fritz_smh to do it")
            # type == url : nothing to do

        except:
            # if an error occurs : return the default value and log a warning
            self.log.warning(u"Error while casting value '{0}' to type '{1}'. The plugin may not work!! Error : {2}".format(value, type, traceback.format_exc()))
            return value
        return value

    def get_device_list(self, quit_if_no_device = False):
        """ Request the dbmgr component over MQ to get the devices list for this client
            @param quit_if_no_device: if True, exit the plugin if there is no devices
        """
        self.log.info(u"Retrieve the devices list for this client...")
        mq_client = MQSyncReq(self.zmq)
        msg = MQMessage()
        msg.set_action('device.get')
        msg.add_data('type', 'plugin')
        msg.add_data('name', self._name)
        msg.add_data('host', self.get_sanitized_hostname())
        result = mq_client.request('dbmgr', msg.get(), timeout=10)
        if not result:
            self.log.error(u"Unable to retrieve the device list")
            self.force_leave()
            return []
        else:
            device_list = result.get_data()['devices']
            if device_list == []:
                self.log.warn(u"There is no device created for this client")
                if quit_if_no_device:
                    self.log.warn(u"The developper requested to stop the client if there is no device created")
                    self.force_leave()
                    return []
            for a_device in device_list:
                print type(a_device['name'])
                self.log.info(u"- id : {0}  /  name : {1}  /  device type id : {2}".format(a_device['id'], \
                                                                                    a_device['name'], \
                                                                                    a_device['device_type_id']))
                # log some informations about the device
                # first : the stats
                self.log.info(u"  Features :")
                for a_xpl_stat in a_device['xpl_stats']:
                    self.log.info(u"  - {0}".format(a_xpl_stat))
                    self.log.info(u"    Parameters :")
                    for a_feature in a_device['xpl_stats'][a_xpl_stat]['parameters']['device']:
                        self.log.info(u"    - {0} = {1}".format(a_feature['key'], a_feature['value']))

                # then, the commands
                # TODO !!!!!!

            self.devices = device_list
            return device_list


    def device_detected(self, device_type, type, feature, data):
        """ The plugin developpers can call this function when a device is detected
            This function will check if a corresponding device exists and : 
            - if so, do nothing
            - if not, add the device in a 'new devices' list
                 - if the device is already in the 'new devices list', does nothing
                 - if not : add it into the list and send a MQ message : an event for the UI to say a new device is detected

            ### TODO : implement a req/rep MQ message to allow UI to get the new devices list

            @param device_type : device_type of the detected device
            @param data : data about the device (address or any other configuration element of a device for this plugin)
            @param type : xpl_stats, xpl_commands
            @param feature : a xpl_stat or xpl_command feature
        """
        self.log.debug(u"Device detected : device_type = {0}, data = {1}".format(device_type, data))
        #self.log.debug(u"Already existing devices : {0}".format(self.devices))
        # browse all devices to find if the device exists
        for a_device in self.devices:
            # first, search for device type
            if a_device['device_type_id'] == device_type:
                params = a_device[type][feature]['parameters']['device']
                found = True
                for key in data:
                    for a_param in params:
                        if key == a_param['key'] and data[key] != a_param['value']:
                            found = False
                            break
                if found:
                    break
        if found:
            self.log.debug(u"The device already exists : id={0}.".format(a_device['id']))
        else:
            self.log.debug(u"The device doesn't exists in database")
         
            # add the device feature in the new devices list : self.new_devices[device_type][type][feature] = data
            self.log.debug(u"Check if the device has already be marked as new...")
            found = False
            for a_device in self.new_devices:
                if a_device['device_type_id'] == device_type and \
                   a_device['type'] == type and \
                   a_device['feature'] == feature:

                   if data == a_device['data']:
                        found = True
                    
            if found == False:
                new_device ={'device_type_id' : device_type,
                             'type' : type,
                             'feature' : feature,
                             'data' : data}
                self.log.info(u"New device feature detected and added in the new devices list : {0}".format(new_device))
                self.new_devices.append(new_device)
            else:
                self.log.debug(u"The device has already been detected since the plugin startup")
            print self.new_devices







    def get_parameter_for_feature(self, a_device, type, feature, key):
        """ For a device feature, return the required parameter value
            Example with : a_device = {u'xpl_stats': {u'get_total_space': {u'name': u'get_total_space', u'id': 49, u'parameters': {u'device': [{u'xplstat_id': 49, u'key': u'device', u'value': u'/home'}, {u'xplstat_id': 49, u'key': u'interval', u'value': u'1'}], u'static': [{u'xplstat_id': 49, u'key': u'type', u'value': u'total_space'}], u'dynamic': [{u'xplstat_id': 49, u'ignore_values': u'', u'key': u'current', u'value': None}]}, u'schema': u'sensor.basic'}, u'get_free_space': {u'name': u'get_free_space', u'id':51, u'parameters': {u'device': [{u'xplstat_id': 51, u'key': u'device', u'value': u'/home'}, {u'xplstat_id': 51, u'key': u'interval', u'value': u'1'}], u'static': [{u'xplstat_id': 51, u'key': u'type', u'value': u'free_space'}], u'dynamic': [ {u'xplstat_id': 51, u'ignore_values': u'', u'key': u'current', u'value': None}]}, u'schema': u'sensor.basic'}, u'get_used_space': {u'name': u'get_used_space', u'id': 52, u'parameters': {u'device': [{u'xplstat_id': 52, u'key': u'device', u'value': u'/home'}, {u'xplstat_id': 52, u'key': u'interval', u'value': u'1'}], u'static': [{u'xplstat_id': 52, u'key': u'type', u'value': u'used_space'}], u'dynamic': [{u'xplstat_id': 52, u'ignore_values': u'', u'key': u'current', u'value': None}]}, u'schema': u'sensor.basic'}, u'get_percent_used': {u'name': u'get_percent_used', u'id': 50, u'parameters': {u'device': [{u'xplstat_id': 50, u'key': u'device', u'value': u'/home'}, {u'xplstat_id': 50, u'key': u'interval', u'value': u'1'}], u'static': [{u'xplstat_id': 50, u'key': u'type', u'value': u'percent_used'}], u'dynamic': [{u'xplstat_id': 50, u'ignore_values': u'', u'key': u'current', u'value': None}]}, u'schema': u'sensor.basic'}}, u'commands': {}, u'description': u'/home sur darkstar', u'reference': u'ref', u'id': 49, u'device_type_id': u'diskfree.disk_usage', u'sensors': {u'get_total_space': {u'conversion': u'', u'name': u'Total Space', u'data_type': u'DT_Scaling', u'last_received': None, u'last_value': None, u'id': 80}, u'get_free_space': {u'conversion': u'', u'name': u'Free Space', u'data_type': u'DT_Scaling', u'last_received': None, u'last_value': None, u'id': 82}, u'get_used_space': {u'conversion': u'', u'name': u'Used Space', u'data_type': u'DT_Scaling', u'last_received': None, u'last_value': None, u'id': 83}, u'get_percent_used': {u'conversion': u'', u'name': u'Percent used', u'data_type': u'DT_Scaling', u'last_received': None, u'last_value': None, u'id': 81}}, u'client_id': u'domogik-diskfree.darkstar', u'name': u'darkstar:/home'}
                         type = xpl_stats
                         feature = get_percent_used
                         key = device
            Return : /home
        """
        try:
            self.log.debug(u"Get parameter '{0}' for '{1}', feature '{2}'".format(key, type, feature))
            for a_param in a_device[type][feature]['parameters']['device']:
                if a_param['key'] == key:
                    value = self.cast(a_param['value'], a_param['type'])
                    self.log.debug(u"Parameter value found: {0}".format(value))
                    return value
            self.log.warning(u"Parameter not found : return None")
            return None
        except:
            self.log.error(u"Error while looking for a device feature parameter. Return None. Error: {0}".format(traceback.format_exc()))
            return None
         

    def check_for_pictures(self):
        """ if some products are defined, check if the corresponding pictures are present in the products/ folder
        """
        self.log.info(u"Check if there are pictures for the defined products")
        ok = True
        ok_product = None
        if self.json_data.has_key('products'):
            for product in self.json_data['products']:
                ok_product = False
                for ext in PRODUCTS_PICTURES_EXTENSIONS:
                    file = "{0}.{1}".format(product['id'], ext)
                    if os.path.isfile("{0}/{1}".format(self.get_products_directory(), file)):
                        ok_product = True
                        break
                if ok_product:
                    self.log.debug(u"- OK : {0} ({1})".format(product['name'], file))
                else:
                    ok = False
                    self.log.warning(u"- Missing : {0} ({1}.{2})".format(product['name'], product['id'], PRODUCTS_PICTURES_EXTENSIONS))
        if ok == False:
            self.log.warning(u"Some pictures are missing!")
        else:
            if ok_product == None:
                self.log.info(u"There is no products defined for this plugin")


    def ready(self, ioloopstart=1):
        """ to call at the end of the __init__ of classes that inherits of XplPlugin
        """
        if self.dont_run_ready == True:
            return

        ### activate xpl hbeat
        if self.enable_hbeat_called == True:
            self.log.error(u"in ready() : enable_hbeat() function already called : the plugin may not be fully converted to the 0.4+ Domogik format")
        else:
            self.enable_hbeat()

        # send the status for the xpl hbeat
        self.myxpl.update_status(2)

        ### send plugin status : STATUS_ALIVE
        # TODO : why the dbmgr has no self._name defined ???????
        # temporary set as unknown to avoir blocking bugs
        if not hasattr(self, '_name'):
            self._name = "unknown"
        self._set_status(STATUS_ALIVE)

        ### Instantiate the MQ
        # nothing can be launched after this line (blocking call!!!!)
        self.log.info(u"Start IOLoop for MQ : nothing else can be executed in the __init__ after this! Make sure that the self.ready() call is the last line of your init!!!!")
        if ioloopstart == 1:
            IOLoop.instance().start()



    def on_mdp_request(self, msg):
        """ Handle Requests over MQ
            @param msg : MQ req message
        """
        self.log.debug(u"MQ Request received : {0}" . format(str(msg)))

        ### stop the plugin
        if msg.get_action() == "plugin.stop.do":
            self.log.info(u"Plugin stop request : {0}".format(msg))
            self._mdp_reply_plugin_stop(msg)
        elif msg.get_action() == "helper.list.get":
            self.log.info(u"Plugin helper list request : {0}".format(msg))
            self._mdp_reply_helper_list(msg)
        elif msg.get_action() == "helper.help.get":
            self.log.info(u"Plugin helper help request : {0}".format(msg))
            self._mdp_reply_helper_help(msg)
        elif msg.get_action() == "helper.do":
            self.log.info(u"Plugin helper action request : {0}".format(msg))
            self._mdp_reply_helper_do(msg)
    
    def _mdp_reply_helper_do(self, msg):
        contens = msg.get_data()
        if 'command' in contens.keys():
            if contens['command'] in self.helpers.keys():
                if 'params' not in contens.keys():
                    contens['params'] = {}
                    params = []
                else:
                    params = []
                    for key, value in contens['params'].items():
                        params.append( "{0}='{1}'".format(key, value) )
                command = "self.{0}(".format(self.helpers[contens['command']]['call'])
                command += ", ".join(params)
                command += ")"
                print(command)
                result = eval(command)
                # run the command with all params
                msg = MQMessage()
                msg.set_action('helper.do.result')
                msg.add_data('command', contens['command'])
                msg.add_data('params', contens['params'])
                msg.add_data('result', result)
                self.reply(msg.get())

    def _mdp_reply_helper_help(self, data):
        content = data.get_data()
        if 'command' in contens.keys():
            if content['command'] in self.helpers.keys():
                msg = MQMessage()
                msg.set_action('helper.help.result')
                msg.add_data('help', self.helpers[content['command']]['help'])
                self.reply(msg.get())

    def _mdp_reply_plugin_stop(self, data):
        """ Stop the plugin
            @param data : MQ req message

            First, send the MQ Rep to 'ack' the request
            Then, change the plugin status to STATUS_STOP_REQUEST
            Then, quit the plugin by calling force_leave(). This should make the plugin send a STATUS_STOPPED if all is ok

            Notice that no check is done on the MQ req content : we need nothing in it as it is directly addressed to a plugin
        """
        # check if the message is for us
        content = data.get_data()
        if content['name'] != self._name or content['host'] != self.get_sanitized_hostname():
            return

        ### Send the ack over MQ Rep
        msg = MQMessage()
        msg.set_action('plugin.stop.result')
        status = True
        reason = ""
        msg.add_data('status', status)
        msg.add_data('reason', reason)
        self.reply(msg.get())

        ### Change the plugin status
        self._set_status(STATUS_STOP_REQUEST)

        ### Try to stop the plugin
        # if it fails, the manager should try to kill the plugin
        self.force_leave()

    def _mdp_reply_helper_list(self, data):
        """ Return a list of supported helpers
            @param data : MQ req message
        """
        ### Send the ack over MQ Rep
        msg = MQMessage()
        msg.set_action('helper.list.result')
        msg.add_data('actions', self.helpers.keys())
        self.reply(msg.get())

    def _set_status(self, status):
        """ Set the plugin status and send it
        """
        self._status = status
        self._send_status()

    def _send_status_loop(self):
        """ send the status each STATUS_HBEAT seconds
        """
        # TODO : we could optimize by resetting the timer each time the status is sent
        # but as this is used only to check for dead plugins by the manager, it is not very important ;)
        while not self._stop.isSet():
            self._send_status()
            self._stop.wait(STATUS_HBEAT)

    def _send_status(self):
        """ Send the plugin status over the MQ
        """ 
        if hasattr(self, "_pub"):
            if self._name in CORE_COMPONENTS:
                type = "core"
            else:
                type = "plugin"
            self.log.debug("Send plugin status : {0}".format(self._status))
            self._pub.send_event('plugin.status', 
                                 {"type" : type,
                                  "name" : self._name,
                                  "host" : self.get_sanitized_hostname(),
                                  "event" : self._status})

    def get_config_files(self):
       """ Return list of config files
       """
       return self._config_files

    def get_products_directory(self):
       """ getter 
       """
       return self.products_directory

    def get_libraries_directory(self):
       """ getter 
       """
       return self.libraries_directory

    def get_packages_directory(self):
       """ getter 
       """
       return self.packages_directory

    def get_resources_directory(self):
       """ getter 
       """
       return self.resources_directory

    def get_data_files_directory(self):
       """
       Return the directory where a plugin developper can store data files.
       If the directory doesn't exist, try to create it.
       After that, try to create a file inside it.
       If something goes wrong, generate an explicit exception.
       """
       cfg = Loader('domogik')
       my_conf = cfg.load()
       config = dict(my_conf[1])
       path = "{0}/{1}/{2}_{3}/data/" % (self.librairies_directory, PACKAGES_DIR, "plugin", self._name)
       if os.path.exists(path):
           if not os.access(path, os.W_OK & os.X_OK):
               raise OSError("Can't write in directory %s" % path)
       else:
           try:
               os.mkdir(path, '0770')
               self.log.info(u"Create directory %s." % path)
           except:
               raise OSError("Can't create directory %s." % path)
       try:
           tmp_prefix = "write_test";
           count = 0
           filename = os.path.join(path, tmp_prefix)
           while(os.path.exists(filename)):
               filename = "{}.{}".format(os.path.join(path, tmp_prefix),count)
               count = count + 1
           f = open(filename,"w")
           f.close()
           os.remove(filename)
       except :
           raise IOError("Can't create a file in directory %s." % path)
       return path

    def register_helper(self, action, help_string, callback):
        if action not in self.helpers:
            self.helpers[action] = {'call': callback, 'help': help_string}

    def publish_helper(self, key, data):
        if hasattr(self, "_pub"):
            if self._name in CORE_COMPONENTS:
                type = "core"
            else:
                type = "plugin"
            self._pub.send_event('helper.publish',
                                 {"origin" : self._mq_name,
                                  "key": key,
                                  "data": data})

    # TODO :remove
    #def get_stats_files_directory(self):
    #   """ Return the directory where a plugin developper can store data files
    #   """
    #   cfg = Loader('domogik')
    #   my_conf = cfg.load()
    #   config = dict(my_conf[1])
    #   if config.has_key('package_path'):
    #       path = "%s/domogik_packages/stats/%s" % (config['package_path'], self._name)
    #   else:
    #       path = "%s/share/domogik/stats/%s" % (config['src_prefix'], self._name)
    #   return path

    def enable_hbeat(self, lock = False):
        """ Wrapper for xplconnector.enable_hbeat()
        """
        self.myxpl.enable_hbeat(lock)
        self.enable_hbeat_called = True

    def _send_process_info(self, pid, data):
        """ Send process info (cpu, memory) on xpl
            @param : process pid
            @param data : dictionnary of process informations
        """
        mess = XplMessage()
        mess.set_type("xpl-stat")
        mess.set_schema("domogik.usage")
        mess.add_data({"name" : "%s.%s" % (self.get_plugin_name(), self.get_sanitized_hostname()),
                       "pid" : pid,
                       "cpu-percent" : data["cpu_percent"],
                       "memory-percent" : data["memory_percent"],
                       "memory-rss" : data["memory_rss"],
                       "memory-vsz" : data["memory_vsz"]})
        self.myxpl.send(mess)

    def _get_pid(self):
        """ Get current pid and write it to a file
        """
        pid = os.getpid()
        pid_file = os.path.join(self._pid_dir_path,
                                self._name + ".pid")
        self.log.debug(u"Write pid file for pid '%s' in file '%s'" % (str(pid), pid_file))
        fil = open(pid_file, "w")
        fil.write(str(pid))
        fil.close()

    # TODO : remove
    #def _system_handler(self, message):
    #    """ Handler for domogik system messages
    #    """
    #    cmd = message.data['command']
    #    if not self._is_manager and cmd in ["stop", "reload", "dump"]:
    #        self._client_handler(message)
    #    else:
    #        self._manager_handler(message)

    # TODO : remove
    #def _client_handler(self, message):
    #    """ Handle domogik system request for an xpl client
    #    @param message : the Xpl message received
    #    """
    #    try:
    #        cmd = message.data["command"]
    #        plugin = message.data["plugin"]
    #        host = message.data["host"]
    #        if host != self.get_sanitized_hostname():
    #            return
    #    except KeyError as e:
    #        self.log.error(u"command, plugin or host key does not exist : %s", e)
    #        return
    #    if cmd == "stop" and plugin in ['*', self.get_plugin_name()]:
    #        self.log.info(u"Someone asked to stop %s, doing." % self.get_plugin_name())
    #        self._answer_stop()
    #        self.force_leave()
    #    elif cmd == "reload":
    #        if self._reload_cb is None:
    #            self.log.info(u"Someone asked to reload config of %s, but the plugin \
    #            isn't able to do it." % self.get_plugin_name())
    #        else:
    #            self._reload_cb()
    #    elif cmd == "dump":
    #        if self._dump_cb is None:
    #            self.log.info(u"Someone asked to dump config of %s, but the plugin \
    #            isn't able to do it." % self.get_plugin_name())
    #        else:
    #            self._dump_cb()
    #    else: #Command not known
    #        self.log.info(u"domogik.system command not recognized : %s" % cmd)

    def __del__(self):
        if hasattr(self, "log"):
            self.log.debug(u"__del__ Single xpl plugin")
            # we guess that if no "log" is defined, the plugin has not really started, so there is no need to call force leave (and _stop, .... won't be created)
            self.force_leave()

    # TODO : remove
    #def _answer_stop(self):
    #    """ Ack a stop request
    #    """
    #    mess = XplMessage()
    #    mess.set_type("xpl-trig")
    #    mess.set_schema("domogik.system")
    #    #mess.add_data({"command":"stop", "plugin": self.get_plugin_name(),
    #    #    "host": self.get_sanitized_hostname()})
    #    mess.add_data({"command":"stop", 
    #                   "host": self.get_sanitized_hostname(),
    #                   "plugin": self.get_plugin_name()})
    #    self.myxpl.send(mess)

    def _send_hbeat_end(self):
        """ Send the hbeat.end message
        """
        if hasattr(self, "myxpl"):
            mess = XplMessage()
            mess.set_type("xpl-stat")
            mess.set_schema("hbeat.end")
            self.myxpl.send(mess)

    def _manager_handler(self, message):
        """ Handle domogik system request for the Domogik manager
        @param message : the Xpl message received
        """

    def wait(self):
        """ Wait until someone ask the plugin to stop
        """
        self.myxpl._network.join()

    def force_leave(self, status = False):
        '''
        Leave threads & timers
        '''
        # avoid ready() to be launched
        self.dont_run_ready = True
        # stop IOLoop
        #try:
        #    IOLoop.instance().start()
        #except:
        #    pass
        if hasattr(self, "log"):
            self.log.debug(u"force_leave called")
        # send stopped status over the MQ
        if status:
            self._set_status(status)
        else:
            self._set_status(STATUS_STOPPED)

        # send hbeat.end message
        self._send_hbeat_end()

        # try to stop the thread
        try:
            self.get_stop().set()
        except AttributeError:
            pass

        if hasattr(self, "_timers"):
            for t in self._timers:
                if hasattr(self, "log"):
                    self.log.debug(u"Try to stop timer %s"  % t)
                t.stop()
                if hasattr(self, "log"):
                    self.log.debug(u"Timer stopped %s" % t)

        if hasattr(self, "_stop_cb"):
            for cb in self._stop_cb:
                if hasattr(self, "log"):
                    self.log.debug(u"Calling stop additionnal method : %s " % cb.__name__)
                cb()
    
        if hasattr(self, "_threads"):
            for t in self._threads:
                if hasattr(self, "log"):
                    self.log.debug(u"Try to stop thread %s" % t)
                try:
                    t.join()
                except RuntimeError:
                    pass
                if hasattr(self, "log"):
                    self.log.debug(u"Thread stopped %s" % t)
                #t._Thread__stop()
        #Finally, we try to delete all remaining threads
        for t in threading.enumerate():
            if t != threading.current_thread() and t.__class__ != threading._MainThread:
                if hasattr(self, "log"):
                    self.log.info(u"The thread %s was not registered, killing it" % t.name)
                t.join()
                if hasattr(self, "log"):
                    self.log.info(u"Thread %s stopped." % t.name)
        if threading.activeCount() > 1:
            if hasattr(self, "log"):
                self.log.warn(u"There are more than 1 thread remaining : %s" % threading.enumerate())
Beispiel #9
0
class Plugin(BasePlugin, MQRep):
    '''
    Global plugin class, manage signal handlers.
    This class shouldn't be used as-it but should be extended by no xPL plugin or by the class xPL plugin which will be used by the xPL plugins
    This class is a Singleton

    Please keep in mind that the name 'Plugin' is historical. This class is here the base class to use for all kind of 
    clients : plugin (xpl plugin, interface, ...)
    '''


    def __init__(self, name, type = "plugin", stop_cb = None, is_manager = False, parser = None,
                 daemonize = True, log_prefix = "", log_on_stdout = True, test = False):
        '''
        Create Plugin instance, which defines system handlers
        @param name : The name of the current client
        @param type : The type of the current client (default = 'plugin' for xpl plugins
        @param stop_cb : Additionnal method to call when a stop request is received
        @param is_manager : Must be True if the child script is a Domogik Manager process
        You should never need to set it to True unless you develop your own manager
        @param parser : An instance of ArgumentParser. If you want to add extra options to the generic option parser,
        create your own ArgumentParser instance, use parser.add_argument and then pass your parser instance as parameter.
        Your options/params will then be available on self.options and self.args
        @param daemonize : If set to False, force the instance *not* to daemonize, even if '-f' is not passed
        on the command line. If set to True (default), will check if -f was added.
        @param log_prefix : If set, use this prefix when creating the log file in Logger()
        @param log_on_stdout : If set to True, allow to read the logs on both stdout and log file
        '''
        BasePlugin.__init__(self, name, stop_cb, parser, daemonize, log_prefix, log_on_stdout)
        Watcher(self)
        self.log.info(u"----------------------------------")
        self.log.info(u"Starting client '{0}' (new manager instance)".format(name))
        self.log.info(u"Python version is {0}".format(sys.version_info))
        if self.options.test_option:
            self.log.info(u"The client is starting in TEST mode. Test option is {0}".format(self.options.test_option))
        self._type = type
        self._name = name
        self._test = test   # flag used to avoid loading json in test mode
        
        '''
        Calculate the MQ name
        - For a core component this is just its component name (self._name)
        - For a client this is <self._type>-<self._name>-self.hostname

        The reason is that the core components need a fixed name on the mq network,
        if a client starts up it needs to request the config on the network, and it needs to know the worker (core component)
        to ask the config from.

        Because of the above reason, every item in the core_component list can only run once
        '''
        if self._name in CORE_COMPONENTS:
            self._mq_name = self._name
        else:
            self._mq_name = "{0}-{1}.{2}".format(self._type, self._name, self.get_sanitized_hostname())

        # MQ publisher and REP
        self.zmq = zmq.Context()
        self._pub = MQPub(self.zmq, self._mq_name)
        self._set_status(STATUS_STARTING)

        # MQ : start the thread which sends the status each N seconds
        thr_send_status = threading.Thread(None,
                                           self._send_status_loop,
                                           "send_status_loop",
                                           (),
                                           {})
        thr_send_status.start()

        ### MQ
        # for stop requests
        MQRep.__init__(self, self.zmq, self._mq_name)

        self.helpers = {}
        self._is_manager = is_manager
        cfg = Loader('domogik')
        my_conf = cfg.load()
        self._config_files = CONFIG_FILE
        self.config = dict(my_conf[1])
 
        self.libraries_directory = self.config['libraries_path']
        self.packages_directory = "{0}/{1}".format(self.config['libraries_path'], PACKAGES_DIR)
        self.resources_directory = "{0}/{1}".format(self.config['libraries_path'], RESOURCES_DIR)
        self.products_directory = "{0}/{1}_{2}/{3}".format(self.packages_directory, self._type, self._name, PRODUCTS_DIR)

        # client config
        self._client_config = None

        # Get pid and write it in a file
        self._pid_dir_path = self.config['pid_dir_path']
        self._get_pid()

        if len(self.get_sanitized_hostname()) > 16:
            self.log.error(u"You must use 16 char max hostnames ! {0} is {1} long".format(self.get_sanitized_hostname(), len(self.get_sanitized_hostname())))
            self.force_leave()
            return

        # Create object which get process informations (cpu, memory, etc)
        # TODO : activate
        # TODO : use something else that xPL ?????????
        #self._process_info = ProcessInfo(os.getpid(),
        #                                 TIME_BETWEEN_EACH_PROCESS_STATUS,
        #                                 self._send_process_info,
        #                                 self.log,
        #                                 self.myxpl)
        #self._process_info.start()

        self.dont_run_ready = False

        # for all no core elements, load the json
        # TODO find a way to do it nicer ??
        if self._name not in CORE_COMPONENTS and self._test == False:
            self._load_json()

        # init an empty devices list
        self.devices = []
        # init an empty 'new' devices list
        self.new_devices = []

        # check for products pictures
        if self._name not in CORE_COMPONENTS and self._test == False:
            self.check_for_pictures()

        # init finished
        self.log.info(u"End init of the global client part")


    def check_configured(self):
        """ For a client only
            To be call in the client __init__()
            Check in database (over queryconfig) if the key 'configured' is set to True for the client
            if not, stop the client and log this
        """
        self._client_config = Query(self.zmq, self.log)
        configured = self._client_config.query(self._type, self._name, 'configured')
        if configured == '1':
            configured = True
        if configured != True:
            self.log.error(u"The client is not configured (configured = '{0}'. Stopping the client...".format(configured))
            self.force_leave(status = STATUS_NOT_CONFIGURED)
            return False
        self.log.info(u"The client is configured. Continuing (hoping that the user applied the appropriate configuration ;)")
        return True


    def _load_json(self):
        """ Load the client json file
        """
        try:
            self.log.info(u"Read the json file and validate id".format(self._name))
            pkg_json = PackageJson(pkg_type = self._type, name = self._name)
            # check if json is valid
            if pkg_json.validate() == False:
                # TODO : how to get the reason ?
                self.log.error(u"Invalid json file")
                self.force_leave(status = STATUS_INVALID)
            else:
                # if valid, store the data so that it can be used later
                self.log.info(u"The json file is valid")
                self.json_data = pkg_json.get_json()
        except:
            self.log.error(u"Error while trying to read the json file : {1}".format(self._name, traceback.format_exc()))
            self.force_leave(status = STATUS_INVALID)

    def get_config(self, key):
        """ Try to get the config over the MQ. If value is None, get the default value
        """
        if self._client_config == None:
            self._client_config = Query(self.zmq, self.log)
        value = self._client_config.query(self._type, self._name, key)
        if value == None or value == 'None':
            self.log.info(u"Value for '{0}' is None or 'None' : trying to get the default value instead...".format(key))
            value = self.get_config_default_value(key)
        self.log.info(u"Value for '{0}' is : {1}".format(key, value))
        return self.cast_config_value(key, value)

    def get_config_default_value(self, key):
        """ Get the default value for a config key from the json file
            @param key : configuration key
        """
        for idx in range(len(self.json_data['configuration'])):
            if self.json_data['configuration'][idx]['key'] == key:
                default = self.json_data['configuration'][idx]['default']
                self.log.info(u"Default value required for key '{0}' = {1}".format(key, default))
                return default

    def cast_config_value(self, key, value):
        """ Cast the config value as the given type in the json file
            @param key : configuration key
            @param value : configuration value to cast and return
            @return : the casted value
        """
        for idx in range(len(self.json_data['configuration'])):
            if self.json_data['configuration'][idx]['key'] == key:
                type = self.json_data['configuration'][idx]['type']
                self.log.info(u"Casting value for key '{0}' in type '{1}'...".format(key, type)) 
                cvalue =  self.cast(value, type)
                self.log.info(u"Value is : {0}".format(cvalue))
                return cvalue

        # no cast operation : return the value
        if value == "None":
            return None
        return value

    def cast(self, value, type):
        """ Cast a value for a type
            @param value : value to cast
            @param type : type in which you want to cast the value
        """
        try:
            if type == "boolean":
                # just in case, the "True"/"False" are not already converted in True/False
                # this is (currently) done on queryconfig side
                if value in ["True", "Y"]:
                    return True
                elif value in  ["False", "N"]:
                    return False
            # type == choice : nothing to do
            if type == "date": 
                self.log.error(u"TODO : the cast in date format is not yet developped. Please request fritz_smh to do it")
            if type == "datetime": 
                self.log.error(u"TODO : the cast in date format is not yet developped. Please request fritz_smh to do it")
            # type == email : nothing to do
            if type == "float":
                return float(value)
            if type == "integer":
                return float(value)
            # type == ipv4 : nothing to do
            # type == multiple choice : nothing to do
            # type == string : nothing to do
            if type == "time": 
                self.log.error(u"TODO : the cast in date format is not yet developped. Please request fritz_smh to do it")
            # type == url : nothing to do

        except:
            # if an error occurs : return the default value and log a warning
            self.log.warning(u"Error while casting value '{0}' to type '{1}'. The client may not work!! Error : {2}".format(value, type, traceback.format_exc()))
            return value
        return value

    def get_device_list(self, quit_if_no_device = False, max_attempt = 2):
        """ Request the dbmgr component over MQ to get the devices list for this client
            @param quit_if_no_device: if True, exit the client if there is no devices or MQ request fail
            @param max_attempt : number of retry MQ request if it fail
        """
        self.log.info(u"Retrieve the devices list for this client...")
        msg = MQMessage()
        msg.set_action('device.get')
        msg.add_data('type', self._type)
        msg.add_data('name', self._name)
        msg.add_data('host', self.get_sanitized_hostname())
        attempt = 1
        result = None
        while not result and attempt <= max_attempt :
            mq_client = MQSyncReq(self.zmq)
            result = mq_client.request('dbmgr', msg.get(), timeout=10)
            if not result:
                self.log.warn(u"Unable to retrieve the device list (attempt {0}/{1})".format(attempt, max_attempt))
                attempt += 1
        if not result:
            self.log.error(u"Unable to retrieve the device list, max attempt achieved : {0}".format(max_attempt))
            if quit_if_no_device:
                self.log.warn(u"The developper requested to stop the client if error on retrieve the device list")
                self.force_leave()
            return []
        else:
            device_list = result.get_data()['devices']
            if device_list == []:
                self.log.warn(u"There is no device created for this client")
                if quit_if_no_device:
                    self.log.warn(u"The developper requested to stop the client if there is no device created")
                    self.force_leave()
                    return []
            for a_device in device_list:
                self.log.info(u"- id : {0}  /  name : {1}  /  device type id : {2}".format(a_device['id'], \
                                                                                    a_device['name'], \
                                                                                    a_device['device_type_id']))
                # log some informations about the device
                # notice that even if we are not in the XplPlugin class we will display xpl related informations :
                # for some no xpl clients, there will just be nothing to display.

                # first : the stats
                self.log.info(u"  xpl_stats features :")
                for a_xpl_stat in a_device['xpl_stats']:
                    self.log.info(u"  - {0}".format(a_xpl_stat))
                    self.log.info(u"    Static Parameters :")
                    for a_feature in a_device['xpl_stats'][a_xpl_stat]['parameters']['static']:
                        self.log.info(u"    - {0} = {1}".format(a_feature['key'], a_feature['value']))
                    self.log.info(u"    Dynamic Parameters :")
                    for a_feature in a_device['xpl_stats'][a_xpl_stat]['parameters']['dynamic']:
                        self.log.info(u"    - {0}".format(a_feature['key']))

                # then, the commands
                self.log.info(u"  xpl_commands features :")
                for a_xpl_cmd in a_device['xpl_commands']:
                    self.log.info(u" - {0}".format(a_xpl_cmd))
                    self.log.info(u" + Parameters :")
                    for a_feature in a_device['xpl_commands'][a_xpl_cmd]['parameters']:
                        self.log.info(u" - {0} = {1}".format(a_feature['key'], a_feature['value']))

            self.devices = device_list
            return device_list


    def device_detected(self, data):
        """ The clients developpers can call this function when a device is detected
            This function will check if a corresponding device exists and :
            - if so, do nothing
            - if not, add the device in a 'new devices' list
                 - if the device is already in the 'new devices list', does nothing
                 - if not : add it into the list and send a MQ message : an event for the UI to say a new device is detected

            @param data : data about the device 
            
            Data example : 
            {
                "device_type" : "...",
                "reference" : "...",
                "global" : [
                    { 
                        "key" : "....",
                        "value" : "...."
                    },
                    ...
                ],
                "xpl" : [
                    { 
                        "key" : "....",
                        "value" : "...."
                    },
                    ...
                ],
                "xpl_commands" : {
                    "command_id" : [
                        { 
                            "key" : "....",
                            "value" : "...."
                        },
                        ...
                    ],
                    "command_id_2" : [...]
                },
                "xpl_stats" : {
                    "sensor_id" : [
                        { 
                            "key" : "....",
                            "value" : "...."
                        },
                        ...
                    ],
                    "sensor_id_2" : [...]
                }
            }
        """
        self.log.debug(u"Device detected : data = {0}".format(data))
        # browse all devices to find if the device exists
        found = False
        for a_device in self.devices:
            # filter on appropriate device_type
            if a_device['device_type_id'] != data['device_type']:
                continue

            # handle "main" global parameters
            # TODO ????

            # handle xpl global parameters
            if data['xpl'] != []:
                for dev_feature in a_device['xpl_stats']:
                    for dev_param in a_device['xpl_stats'][dev_feature]['parameters']['static']:
                        #print(dev_param)
                        for found_param in data['xpl']:
                            if dev_param['key'] == found_param['key'] and dev_param['value'] == found_param['value']:
                                found = True
                                #print("FOUND")
                                break
                for dev_feature in a_device['xpl_commands']:
                    for dev_param in a_device['xpl_commands'][dev_feature]['parameters']['static']:
                        #print(dev_param)
                        for found_param in data['xpl']:
                            if dev_param['key'] == found_param['key'] and dev_param['value'] == found_param['value']:
                                found = True
                                #print("FOUND")
                                break

            # handle xpl specific parameters
            if not found and data['xpl_stats'] != []:
                for dev_feature in a_device['xpl_stats']:
                    for dev_param in a_device['xpl_stats'][dev_feature]['parameters']['static']:
                        #print(dev_param)
                        for found_param in data['xpl_stats']:
                            if dev_param['key'] == found_param['key'] and dev_param['value'] == found_param['value']:
                                found = True
                                #print("FOUND")
                                break

            if not found and data['xpl_commands'] != []:
                for dev_feature in a_device['xpl_commands']:
                    for dev_param in a_device['xpl_commands'][dev_feature]['parameters']['static']:
                        #print(dev_param)
                        for found_param in data['xpl_commands']:
                            if dev_param['key'] == found_param['key'] and dev_param['value'] == found_param['value']:
                                found = True
                                #print("FOUND")
                                break


        if found:
            self.log.debug(u"The device already exists : id={0}.".format(a_device['id']))
        else:
            self.log.debug(u"The device doesn't exists in database")
            # generate a unique id for the device from its addresses
            new_device_id = self.generate_detected_device_id(data)
         
            # add the device feature in the new devices list : self.new_devices[device_type][type][feature] = data
            self.log.debug(u"Check if the device has already be marked as new...")
            found = False
            for a_device in self.new_devices:
                if a_device['id'] == new_device_id:
                    found = True

            #for a_device in self.new_devices:
            #    if a_device['device_type_id'] == device_type and \
            #       a_device['type'] == type and \
            #       a_device['feature'] == feature:
#
            #       if data == a_device['data']:
            #            found = True
                    
            if found == False:
                new_device = {'id' : new_device_id, 'data' : data}
                self.log.info(u"New device feature detected and added in the new devices list : {0}".format(new_device))
                self.new_devices.append(new_device)

                # publish new devices update
                self._pub.send_event('device.new',
                                     {"type" : self._type,
                                      "name" : self._name,
                                      "host" : self.get_sanitized_hostname(),
                                      "client_id" : "{0}-{1}.{2}".format(self._type, self._name, self.get_sanitized_hostname()),
                                      "device" : new_device})

                # TODO : later (0.4.0+), publish one "new device" notification with only the new device detected

            else:
                self.log.debug(u"The device has already been detected since the client startup")

    def generate_detected_device_id(self, data):
        """ Generate an unique id based on the content of data
        """
        # TODO : improve to make something more sexy ?
        the_id = json.dumps(data, sort_keys=True) 
        chars_to_remove = ['"', '{', '}', ',', ' ', '=', '[', ']', ':']
        the_id = the_id.translate(None, ''.join(chars_to_remove))
        return the_id


    def get_parameter(self, a_device, key):
        """ For a device feature, return the required parameter value
            @param a_device: the device informations
            @param key: the parameter key
        """
        try:
            self.log.debug(u"Get parameter '{0}'".format(key))
            for a_param in a_device['parameters']:
                if a_param == key:
                    value = self.cast(a_device['parameters'][a_param]['value'], a_device['parameters'][a_param]['type'])
                    self.log.debug(u"Parameter value found: {0}".format(value))
                    return value
            self.log.warning(u"Parameter not found : return None")
            return None
        except:
            self.log.error(u"Error while looking for a device parameter. Return None. Error: {0}".format(traceback.format_exc()))
            return None
         

    def get_parameter_for_feature(self, a_device, type, feature, key):
        """ For a device feature, return the required parameter value
            @param a_device: the device informations
            @param type: the parameter type (xpl_stats, ...)
            @param feature: the parameter feature
            @param key: the parameter key
        """
        try:
            self.log.debug(u"Get parameter '{0}' for '{1}', feature '{2}'".format(key, type, feature))
            for a_param in a_device[type][feature]['parameters']['static']:
                if a_param['key'] == key:
                    value = self.cast(a_param['value'], a_param['type'])
                    self.log.debug(u"Parameter value found: {0}".format(value))
                    return value
            self.log.warning(u"Parameter not found : return None")
            return None
        except:
            self.log.error(u"Error while looking for a device feature parameter. Return None. Error: {0}".format(traceback.format_exc()))
            return None
         

    def check_for_pictures(self):
        """ if some products are defined, check if the corresponding pictures are present in the products/ folder
        """
        self.log.info(u"Check if there are pictures for the defined products")
        ok = True
        ok_product = None
        if self.json_data.has_key('products'):
            for product in self.json_data['products']:
                ok_product = False
                for ext in PRODUCTS_PICTURES_EXTENSIONS:
                    file = "{0}.{1}".format(product['id'], ext)
                    if os.path.isfile("{0}/{1}".format(self.get_products_directory(), file)):
                        ok_product = True
                        break
                if ok_product:
                    self.log.debug(u"- OK : {0} ({1})".format(product['name'], file))
                else:
                    ok = False
                    self.log.warning(u"- Missing : {0} ({1}.{2})".format(product['name'], product['id'], PRODUCTS_PICTURES_EXTENSIONS))
        if ok == False:
            self.log.warning(u"Some pictures are missing!")
        else:
            if ok_product == None:
                self.log.info(u"There is no products defined for this client")


    def ready(self, ioloopstart=1):
        """ to call at the end of the __init__ of classes that inherits of this one
 
            In the XplPLugin class, this function will be completed to also activate the xpl hbeat
        """
        if self.dont_run_ready == True:
            return

        ### send client status : STATUS_ALIVE
        # TODO : why the dbmgr has no self._name defined ???????
        # temporary set as unknown to avoir blocking bugs
        if not hasattr(self, '_name'):
            self._name = "unknown"
        self._set_status(STATUS_ALIVE)

        ### Instantiate the MQ
        # nothing can be launched after this line (blocking call!!!!)
        self.log.info(u"Start IOLoop for MQ : nothing else can be executed in the __init__ after this! Make sure that the self.ready() call is the last line of your init!!!!")
        if ioloopstart == 1:
            IOLoop.instance().start()



    def on_mdp_request(self, msg):
        """ Handle Requests over MQ
            @param msg : MQ req message
        """
        self.log.debug(u"MQ Request received : {0}" . format(str(msg)))

        ### stop the client
        if msg.get_action() == "plugin.stop.do":
            self.log.info(u"Client stop request : {0}".format(msg))
            self._mdp_reply_client_stop(msg)
        elif msg.get_action() == "helper.list.get":
            self.log.info(u"Client helper list request : {0}".format(msg))
            self._mdp_reply_helper_list(msg)
        elif msg.get_action() == "helper.help.get":
            self.log.info(u"Client helper help request : {0}".format(msg))
            self._mdp_reply_helper_help(msg)
        elif msg.get_action() == "helper.do":
            self.log.info(u"Client helper action request : {0}".format(msg))
            self._mdp_reply_helper_do(msg)
        elif msg.get_action() == "device.new.get":
            self.log.info(u"Client new devices request : {0}".format(msg))
            self._mdp_reply_device_new_get(msg)
    
    def _mdp_reply_helper_do(self, msg):
        contens = msg.get_data()
        if 'command' in contens.keys():
            if contens['command'] in self.helpers.keys():
                if 'parameters' not in contens.keys():
                    contens['parameters'] = {}
                    params = []
                else:
                    params = []
                    for key, value in contens['parameters'].items():
                        params.append( "{0}='{1}'".format(key, value) )
                command = "self.{0}(".format(self.helpers[contens['command']]['call'])
                command += ", ".join(params)
                command += ")"
                result = eval(command)
                # run the command with all params
                msg = MQMessage()
                msg.set_action('helper.do.result')
                msg.add_data('command', contens['command'])
                msg.add_data('parameters', contens['parameters'])
                msg.add_data('result', result)
                self.reply(msg.get())

    def _mdp_reply_helper_help(self, data):
        content = data.get_data()
        if 'command' in contens.keys():
            if content['command'] in self.helpers.keys():
                msg = MQMessage()
                msg.set_action('helper.help.result')
                msg.add_data('help', self.helpers[content['command']]['help'])
                self.reply(msg.get())

    def _mdp_reply_client_stop(self, data):
        """ Stop the client
            @param data : MQ req message

            First, send the MQ Rep to 'ack' the request
            Then, change the client status to STATUS_STOP_REQUEST
            Then, quit the client by calling force_leave(). This should make the client send a STATUS_STOPPED if all is ok

            Notice that no check is done on the MQ req content : we need nothing in it as it is directly addressed to a client
        """
        # check if the message is for us
        content = data.get_data()
        if content['name'] != self._name or content['host'] != self.get_sanitized_hostname():
            return

        ### Send the ack over MQ Rep
        msg = MQMessage()
        msg.set_action('plugin.stop.result')
        status = True
        reason = ""
        msg.add_data('status', status)
        msg.add_data('reason', reason)
        msg.add_data('name', self._name)
        msg.add_data('host', self.get_sanitized_hostname())
        self.log.info("Send reply for the stop request : {0}".format(msg))
        self.reply(msg.get())

        ### Change the client status
        self._set_status(STATUS_STOP_REQUEST)

        ### Try to stop the client
        # if it fails, the manager should try to kill the client
        self.force_leave()

    def _mdp_reply_helper_list(self, data):
        """ Return a list of supported helpers
            @param data : MQ req message
        """
        ### Send the ack over MQ Rep
        msg = MQMessage()
        msg.set_action('helper.list.result')
        msg.add_data('actions', self.helpers.keys())
        self.reply(msg.get())

    def _mdp_reply_device_new_get(self, data):
        """ Return a list of new devices detected
            @param data : MQ req message
        """
        ### Send the ack over MQ Rep
        msg = MQMessage()
        msg.set_action('device.new.result')
        msg.add_data('devices', self.new_devices)
        self.reply(msg.get())


    def _set_status(self, status):
        """ Set the client status and send it
        """
        # when ctrl-c is done, there is no more self._name at this point...
        # why ? because the force_leave method is called twice as show in the logs : 
        #
        # ^CKeyBoardInterrupt
        # 2013-12-20 22:48:41,040 domogik-manager INFO Keyboard Interrupt detected, leave now.
        # Traceback (most recent call last):
        #   File "./manager.py", line 1176, in <module>
        #     main()
        #   File "./manager.py", line 1173, in main
        # 2013-12-20 22:48:41,041 domogik-manager DEBUG force_leave called
        # 2013-12-20 22:48:41,044 domogik-manager DEBUG __del__ Single xpl plugin
        # 2013-12-20 22:48:41,045 domogik-manager DEBUG force_leave called

        if hasattr(self, '_name'):
            #if self._name not in CORE_COMPONENTS:
            #    self._status = status
            #    self._send_status()
            self._status = status
            self._send_status()

    def _send_status_loop(self):
        """ send the status each STATUS_HBEAT seconds
        """
        # TODO : we could optimize by resetting the timer each time the status is sent
        # but as this is used only to check for dead clients by the manager, it is not very important ;)
        while not self._stop.isSet():
            self._send_status()
            self._stop.wait(STATUS_HBEAT)

    def _send_status(self):
        """ Send the client status over the MQ
        """ 
        if hasattr(self, "_pub"):
            if self._name in CORE_COMPONENTS:
                type = "core"
                #return
            else:
                type = self._type
            self.log.debug("Send client status : {0}".format(self._status))
            self._pub.send_event('plugin.status', 
                                 {"type" : type,
                                  "name" : self._name,
                                  "host" : self.get_sanitized_hostname(),
                                  "event" : self._status})

    def get_config_files(self):
       """ Return list of config files
       """
       return self._config_files

    def get_products_directory(self):
       """ getter 
       """
       return self.products_directory

    def get_libraries_directory(self):
       """ getter 
       """
       return self.libraries_directory

    def get_packages_directory(self):
       """ getter 
       """
       return self.packages_directory

    def get_resources_directory(self):
       """ getter 
       """
       return self.resources_directory

    def get_data_files_directory(self):
       """
       Return the directory where a client developper can store data files.
       If the directory doesn't exist, try to create it.
       After that, try to create a file inside it.
       If something goes wrong, generate an explicit exception.
       """
       path = "{0}/{1}/{2}_{3}/data/".format(self.libraries_directory, PACKAGES_DIR, self._type, self._name)
       if os.path.exists(path):
           if not os.access(path, os.W_OK & os.X_OK):
               raise OSError("Can't write in directory {0}".format(path))
       else:
           try:
               os.mkdir(path, '0770')
               self.log.info(u"Create directory {0}.".format(path))
           except:
               raise OSError("Can't create directory {0}.".format(path))
       # Commented because :
       # a write test is done for each call of this function. For a client with a html server (geoloc for example), it
       # can be an issue as this makes a lot of write for 'nothing' on the disk.
       # We keep the code for now (0.4) for maybe a later use (and improved)
       #try:
       #    tmp_prefix = "write_test";
       #    count = 0
       #    filename = os.path.join(path, tmp_prefix)
       #    while(os.path.exists(filename)):
       #        filename = "{}.{}".format(os.path.join(path, tmp_prefix),count)
       #        count = count + 1
       #    f = open(filename,"w")
       #    f.close()
       #    os.remove(filename)
       #except :
       #    raise IOError("Can't create a file in directory {0}.".format(path))
       return path

    def register_helper(self, action, help_string, callback):
        if action not in self.helpers:
            self.helpers[action] = {'call': callback, 'help': help_string}

    def publish_helper(self, key, data):
        if hasattr(self, "_pub"):
            if self._name in CORE_COMPONENTS:
                type = "core"
            else:
                type = self._type
            self._pub.send_event('helper.publish',
                                 {"origin" : self._mq_name,
                                  "key": key,
                                  "data": data})

    def _get_pid(self):
        """ Get current pid and write it to a file
        """
        pid = os.getpid()
        pid_file = os.path.join(self._pid_dir_path,
                                self._name + ".pid")
        self.log.debug(u"Write pid file for pid '{0}' in file '{1}'".format(str(pid), pid_file))
        fil = open(pid_file, "w")
        fil.write(str(pid))
        fil.close()

    def __del__(self):
        if hasattr(self, "log"):
            self.log.debug(u"__del__ Single client")
            self.log.debug(u"the stack is :")
            for elt in inspect.stack():
                self.log.debug(u"    {0}".format(elt))
            # we guess that if no "log" is defined, the client has not really started, so there is no need to call force leave (and _stop, .... won't be created)
            self.force_leave()

    def force_leave(self, status = False, return_code = None):
        """ Leave threads & timers

            In the XplPLugin class, this function will be completed to also activate the xpl hbeat
        """
        if hasattr(self, "log"):
            self.log.debug(u"force_leave called")
            #self.log.debug(u"the stack is : {0}".format(inspect.stack()))
            self.log.debug(u"the stack is :")
            for elt in inspect.stack():
                self.log.debug(u"    {0}".format(elt))

        if return_code != None:
            self.set_return_code(return_code)
            self.log.info("Return code set to {0} when calling force_leave()".format(return_code))


        # avoid ready() to be launched
        self.dont_run_ready = True
        # stop IOLoop
        #try:
        #    IOLoop.instance().start()
        #except:
        #    pass
        # send stopped status over the MQ
        if status:
            self._set_status(status)
        else:
            self._set_status(STATUS_STOPPED)

        # try to stop the thread
        try:
            self.get_stop().set()
        except AttributeError:
            pass

        if hasattr(self, "_timers"):
            for t in self._timers:
                if hasattr(self, "log"):
                    self.log.debug(u"Try to stop timer {0}".format(t))
                t.stop()
                if hasattr(self, "log"):
                    self.log.debug(u"Timer stopped {0}".format(t))

        if hasattr(self, "_stop_cb"):
            for cb in self._stop_cb:
                if hasattr(self, "log"):
                    self.log.debug(u"Calling stop additionnal method : {0} ".format(cb.__name__))
                cb()
    
        if hasattr(self, "_threads"):
            for t in self._threads:
                if hasattr(self, "log"):
                    self.log.debug(u"Try to stop thread {0}".format(t))
                try:
                    t.join()
                except RuntimeError:
                    pass
                if hasattr(self, "log"):
                    self.log.debug(u"Thread stopped {0}".format(t))
                #t._Thread__stop()

        #Finally, we try to delete all remaining threads
        for t in threading.enumerate():
            if t != threading.current_thread() and t.__class__ != threading._MainThread:
                if hasattr(self, "log"):
                    self.log.info(u"The thread {0} was not registered, killing it".format(t.name))
                t.join()
                if hasattr(self, "log"):
                    self.log.info(u"Thread {0} stopped.".format(t.name))

        if threading.activeCount() > 1:
            if hasattr(self, "log"):
                self.log.warn(u"There are more than 1 thread remaining : {0}".format(threading.enumerate()))
Beispiel #10
0
class XplPlugin(BasePlugin, MQRep):
    '''
    Global plugin class, manage signal handlers.
    This class shouldn't be used as-it but should be extended by xPL plugin
    This class is a Singleton
    '''


    def __init__(self, name, stop_cb = None, is_manager = False, reload_cb = None, dump_cb = None, parser = None,
                 daemonize = True, nohub = False, test = False):
        '''
        Create XplPlugin instance, which defines system handlers
        @param name : The name of the current plugin
        @param stop_cb : Additionnal method to call when a stop request is received
        @param is_manager : Must be True if the child script is a Domogik Manager process
        You should never need to set it to True unless you develop your own manager
        @param reload_cb : Callback to call when a "RELOAD" order is received, if None,
        nothing will happen
        @param dump_cb : Callback to call when a "DUMP" order is received, if None,
        nothing will happen
        @param parser : An instance of ArgumentParser. If you want to add extra options to the generic option parser,
        create your own ArgumentParser instance, use parser.add_argument and then pass your parser instance as parameter.
        Your options/params will then be available on self.options and self.args
        @param daemonize : If set to False, force the instance *not* to daemonize, even if '-f' is not passed
        on the command line. If set to True (default), will check if -f was added.
        @param nohub : if set the hub discovery will be disabled
        '''
        BasePlugin.__init__(self, name, stop_cb, parser, daemonize)
        Watcher(self)
        self.log.info(u"----------------------------------")
        self.log.info(u"Starting plugin '%s' (new manager instance)" % name)
        self.log.info(u"Python version is {0}".format(sys.version_info))
        if self.options.test_option:
            self.log.info(u"The plugin is starting in TEST mode. Test option is {0}".format(self.options.test_option))
        self._name = name
        self._test = test   # flag used to avoid loading json in test mode
        
        '''
        Calculate the MQ name
        - For a core component this is just its component name (self._name)
        - For a plugin this is plugin-<self._name>-self.hostname

        The reason is that the core components need a fixed name on the mq network,
        if a plugin starts up it needs to request the config on the network, and it needs to know the worker (core component)
        to ask the config from.

        Because of the above reason, every item in the core_component list can only run once
        '''
        if self._name in CORE_COMPONENTS:
            self._mq_name = self._name
        else:
            self._mq_name = "plugin-{0}.{1}".format(self._name, self.get_sanitized_hostname())

        # MQ publisher and REP
        self.zmq = zmq.Context()
        self._pub = MQPub(self.zmq, self._mq_name)
        self._set_status(STATUS_STARTING)

        # MQ : start the thread which sends the status each N seconds
        thr_send_status = threading.Thread(None,
                                           self._send_status_loop,
                                           "send_status_loop",
                                           (),
                                           {})
        thr_send_status.start()

        ### MQ
        # for stop requests
        MQRep.__init__(self, self.zmq, self._mq_name)

        self.helpers = {}
        self._is_manager = is_manager
        cfg = Loader('domogik')
        my_conf = cfg.load()
        self._config_files = CONFIG_FILE
        config = dict(my_conf[1])
 
        self.libraries_directory = config['libraries_path']
        self.packages_directory = "{0}/{1}".format(config['libraries_path'], PACKAGES_DIR)
        self.resources_directory = "{0}/{1}".format(config['libraries_path'], RESOURCES_DIR)
        self.products_directory = "{0}/{1}_{2}/{3}".format(self.packages_directory, "plugin", self._name, PRODUCTS_DIR)

        # Get pid and write it in a file
        self._pid_dir_path = config['pid_dir_path']
        self._get_pid()

        if len(self.get_sanitized_hostname()) > 16:
            self.log.error(u"You must use 16 char max hostnames ! %s is %s long" % (self.get_sanitized_hostname(), len(self.get_sanitized_hostname())))
            self.force_leave()
            return

        if 'broadcast' in config:
            broadcast = config['broadcast']
        else:
            broadcast = "255.255.255.255"
        if 'bind_interface' in config:
            self.myxpl = Manager(config['bind_interface'], broadcast = broadcast, plugin = self, nohub = nohub)
        else:
            self.myxpl = Manager(broadcast = broadcast, plugin = self, nohub = nohub)

        self._reload_cb = reload_cb
        self._dump_cb = dump_cb

        # Create object which get process informations (cpu, memory, etc)
        # TODO : activate
        #self._process_info = ProcessInfo(os.getpid(),
        #                                 TIME_BETWEEN_EACH_PROCESS_STATUS,
        #                                 self._send_process_info,
        #                                 self.log,
        #                                 self.myxpl)
        #self._process_info.start()

        self.enable_hbeat_called = False
        self.dont_run_ready = False

        # for all no core elements, load the json
        # TODO find a way to do it nicer ??
        if self._name not in CORE_COMPONENTS and self._test == False:
            self._load_json()

        # init an empty devices list
        self.devices = []
        # init an empty 'new' devices list
        self.new_devices = []

        # check for products pictures
        if self._name not in CORE_COMPONENTS and self._test == False:
            self.check_for_pictures()

        # init finished
        self.log.debug(u"end single xpl plugin")


    def check_configured(self):
        """ For a plugin only
            To be call in the plugin __init__()
            Check in database (over queryconfig) if the key 'configured' is set to True for the plugin
            if not, stop the plugin and log this
        """
        self._config = Query(self.zmq, self.log)
        configured = self._config.query(self._name, 'configured')
        if configured == '1':
            configured = True
        if configured != True:
            self.log.error(u"The plugin is not configured (configured = '{0}'. Stopping the plugin...".format(configured))
            self.force_leave(status = STATUS_NOT_CONFIGURED)
            return False
        self.log.info(u"The plugin is configured. Continuing (hoping that the user applied the appropriate configuration ;)")
        return True


    def _load_json(self):
        """ Load the plugin json file
        """
        try:
            self.log.info(u"Read the json file and validate id".format(self._name))
            pkg_json = PackageJson(pkg_type = "plugin", name = self._name)
            # check if json is valid
            if pkg_json.validate() == False:
                # TODO : how to get the reason ?
                self.log.error(u"Invalid json file")
                self.force_leave(status = STATUS_INVALID)
            else:
                # if valid, store the data so that it can be used later
                self.log.info(u"The json file is valid")
                self.json_data = pkg_json.get_json()
        except:
            self.log.error(u"Error while trying to read the json file : {1}".format(self._name, traceback.format_exc()))
            self.force_leave(status = STATUS_INVALID)

    def get_config(self, key):
        """ Try to get the config over the MQ. If value is None, get the default value
        """
        value = self._config.query(self._name, key)
        if value == None or value == 'None':
            self.log.info(u"Value for '{0}' is None or 'None' : trying to get the default value instead...".format(key))
            value = self.get_config_default_value(key)
        self.log.info(u"Value for '{0}' is : {1}".format(key, value))
        return self.cast_config_value(key, value)

    def get_config_default_value(self, key):
        """ Get the default value for a config key from the json file
            @param key : configuration key
        """
        for idx in range(len(self.json_data['configuration'])):
            if self.json_data['configuration'][idx]['key'] == key:
                default = self.json_data['configuration'][idx]['default']
                self.log.info(u"Default value required for key '{0}' = {1}".format(key, default))
                return default

    def cast_config_value(self, key, value):
        """ Cast the config value as the given type in the json file
            @param key : configuration key
            @param value : configuration value to cast and return
            @return : the casted value
        """
        for idx in range(len(self.json_data['configuration'])):
            if self.json_data['configuration'][idx]['key'] == key:
                type = self.json_data['configuration'][idx]['default']
                self.log.info(u"Casting value for key '{0}' in type '{1}'...".format(key, type)) 
                return self.cast(value, type)

        # no cast operation : return the value
        if value == "None":
            return None
        return value

    def cast(self, value, type):
        """ Cast a value for a type
            @param value : value to cast
            @param type : type in which you want to cast the value
        """
        try:
            if type == "boolean":
                # just in case, the "True"/"False" are not already converted in True/False
                # this is (currently) done on queryconfig side
                if value == "True":
                    return True
                elif value ==  "False":
                    return False
            # type == choice : nothing to do
            if type == "date": 
                self.log.error(u"TODO : the cast in date format is not yet developped. Please request fritz_smh to do it")
            if type == "datetime": 
                self.log.error(u"TODO : the cast in date format is not yet developped. Please request fritz_smh to do it")
            # type == email : nothing to do
            if type == "float":
                return float(value)
            if type == "integer":
                return float(value)
            # type == ipv4 : nothing to do
            # type == multiple choice : nothing to do
            # type == string : nothing to do
            if type == "time": 
                self.log.error(u"TODO : the cast in date format is not yet developped. Please request fritz_smh to do it")
            # type == url : nothing to do

        except:
            # if an error occurs : return the default value and log a warning
            self.log.warning(u"Error while casting value '{0}' to type '{1}'. The plugin may not work!! Error : {2}".format(value, type, traceback.format_exc()))
            return value
        return value

    def get_device_list(self, quit_if_no_device = False):
        """ Request the dbmgr component over MQ to get the devices list for this client
            @param quit_if_no_device: if True, exit the plugin if there is no devices
        """
        self.log.info(u"Retrieve the devices list for this client...")
        mq_client = MQSyncReq(self.zmq)
        msg = MQMessage()
        msg.set_action('device.get')
        msg.add_data('type', 'plugin')
        msg.add_data('name', self._name)
        msg.add_data('host', self.get_sanitized_hostname())
        result = mq_client.request('dbmgr', msg.get(), timeout=10)
        if not result:
            self.log.error(u"Unable to retrieve the device list")
            self.force_leave()
            return []
        else:
            device_list = result.get_data()['devices']
            if device_list == []:
                self.log.warn(u"There is no device created for this client")
                if quit_if_no_device:
                    self.log.warn(u"The developper requested to stop the client if there is no device created")
                    self.force_leave()
                    return []
            for a_device in device_list:
                self.log.info(u"- id : {0}  /  name : {1}  /  device type id : {2}".format(a_device['id'], \
                                                                                    a_device['name'], \
                                                                                    a_device['device_type_id']))
                # log some informations about the device
                # first : the stats
                self.log.info(u"  xpl_stats features :")
                for a_xpl_stat in a_device['xpl_stats']:
                    self.log.info(u"  - {0}".format(a_xpl_stat))
                    self.log.info(u"    Static Parameters :")
                    for a_feature in a_device['xpl_stats'][a_xpl_stat]['parameters']['static']:
                        self.log.info(u"    - {0} = {1}".format(a_feature['key'], a_feature['value']))
                    self.log.info(u"    Dynamic Parameters :")
                    for a_feature in a_device['xpl_stats'][a_xpl_stat]['parameters']['dynamic']:
                        self.log.info(u"    - {0}".format(a_feature['key']))

                # then, the commands
                self.log.info(u"  xpl_commands features :")
                for a_xpl_cmd in a_device['xpl_commands']:
                    self.log.info(u" - {0}".format(a_xpl_cmd))
                    self.log.info(u" + Parameters :")
                    for a_feature in a_device['xpl_commands'][a_xpl_cmd]['parameters']:
                        self.log.info(u" - {0} = {1}".format(a_feature['key'], a_feature['value']))

            self.devices = device_list
            return device_list


    def device_detected(self, device_type, type, feature, data):
        """ The plugin developpers can call this function when a device is detected
            This function will check if a corresponding device exists and : 
            - if so, do nothing
            - if not, add the device in a 'new devices' list
                 - if the device is already in the 'new devices list', does nothing
                 - if not : add it into the list and send a MQ message : an event for the UI to say a new device is detected

            ### TODO : implement a req/rep MQ message to allow UI to get the new devices list

            @param device_type : device_type of the detected device
            @param data : data about the device (address or any other configuration element of a device for this plugin)
            @param type : xpl_stats, xpl_commands
            @param feature : a xpl_stat or xpl_command feature
        """
        self.log.debug(u"Device detected : device_type = {0}, data = {1}".format(device_type, data))
        #self.log.debug(u"Already existing devices : {0}".format(self.devices))
        # browse all devices to find if the device exists
        found = False
        for a_device in self.devices:
            # first, search for device type
            if a_device['device_type_id'] == device_type:
                params = a_device[type][feature]['parameters']['static']
                found = True
                for key in data:
                    for a_param in params:
                        if key == a_param['key'] and data[key] != a_param['value']:
                            found = False
                            break
                if found:
                    break
        if found:
            self.log.debug(u"The device already exists : id={0}.".format(a_device['id']))
        else:
            self.log.debug(u"The device doesn't exists in database")
         
            # add the device feature in the new devices list : self.new_devices[device_type][type][feature] = data
            self.log.debug(u"Check if the device has already be marked as new...")
            found = False
            for a_device in self.new_devices:
                if a_device['device_type_id'] == device_type and \
                   a_device['type'] == type and \
                   a_device['feature'] == feature:

                   if data == a_device['data']:
                        found = True
                    
            if found == False:
                new_device ={'device_type_id' : device_type,
                             'type' : type,
                             'feature' : feature,
                             'data' : data}
                self.log.info(u"New device feature detected and added in the new devices list : {0}".format(new_device))
                self.new_devices.append(new_device)

                # publish new devices update
                self._pub.send_event('device.new',
                                     {"type" : "plugin",
                                      "name" : self._name,
                                      "host" : self.get_sanitized_hostname(),
                                      "client_id" : "plugin-{0}.{1}".format(self._name, self.get_sanitized_hostname()),
                                      "devices" : self.new_devices})

                # TODO : later (0.4.0+), publish one "new device" notification with only the new device detected

            else:
                self.log.debug(u"The device has already been detected since the plugin startup")


    def get_parameter(self, a_device, key):
        """ For a device feature, return the required parameter value
            @param a_device: the device informations
            @param key: the parameter key
        """
        try:
            self.log.debug(u"Get parameter '{0}'".format(key))
            for a_param in a_device['parameters']:
                if a_param == key:
                    value = self.cast(a_device['parameters'][a_param]['value'], a_device['parameters'][a_param]['type'])
                    self.log.debug(u"Parameter value found: {0}".format(value))
                    return value
            self.log.warning(u"Parameter not found : return None")
            return None
        except:
            self.log.error(u"Error while looking for a device parameter. Return None. Error: {0}".format(traceback.format_exc()))
            return None
         

    def get_parameter_for_feature(self, a_device, type, feature, key):
        """ For a device feature, return the required parameter value
            @param a_device: the device informations
            @param type: the parameter type (xpl_stats, ...)
            @param feature: the parameter feature
            @param key: the parameter key
        """
        try:
            self.log.debug(u"Get parameter '{0}' for '{1}', feature '{2}'".format(key, type, feature))
            for a_param in a_device[type][feature]['parameters']['static']:
                if a_param['key'] == key:
                    value = self.cast(a_param['value'], a_param['type'])
                    self.log.debug(u"Parameter value found: {0}".format(value))
                    return value
            self.log.warning(u"Parameter not found : return None")
            return None
        except:
            self.log.error(u"Error while looking for a device feature parameter. Return None. Error: {0}".format(traceback.format_exc()))
            return None
         

    def check_for_pictures(self):
        """ if some products are defined, check if the corresponding pictures are present in the products/ folder
        """
        self.log.info(u"Check if there are pictures for the defined products")
        ok = True
        ok_product = None
        if self.json_data.has_key('products'):
            for product in self.json_data['products']:
                ok_product = False
                for ext in PRODUCTS_PICTURES_EXTENSIONS:
                    file = "{0}.{1}".format(product['id'], ext)
                    if os.path.isfile("{0}/{1}".format(self.get_products_directory(), file)):
                        ok_product = True
                        break
                if ok_product:
                    self.log.debug(u"- OK : {0} ({1})".format(product['name'], file))
                else:
                    ok = False
                    self.log.warning(u"- Missing : {0} ({1}.{2})".format(product['name'], product['id'], PRODUCTS_PICTURES_EXTENSIONS))
        if ok == False:
            self.log.warning(u"Some pictures are missing!")
        else:
            if ok_product == None:
                self.log.info(u"There is no products defined for this plugin")


    def ready(self, ioloopstart=1):
        """ to call at the end of the __init__ of classes that inherits of XplPlugin
        """
        if self.dont_run_ready == True:
            return

        ### activate xpl hbeat
        if self.enable_hbeat_called == True:
            self.log.error(u"in ready() : enable_hbeat() function already called : the plugin may not be fully converted to the 0.4+ Domogik format")
        else:
            self.enable_hbeat()

        # send the status for the xpl hbeat
        self.myxpl.update_status(2)

        ### send plugin status : STATUS_ALIVE
        # TODO : why the dbmgr has no self._name defined ???????
        # temporary set as unknown to avoir blocking bugs
        if not hasattr(self, '_name'):
            self._name = "unknown"
        self._set_status(STATUS_ALIVE)

        ### Instantiate the MQ
        # nothing can be launched after this line (blocking call!!!!)
        self.log.info(u"Start IOLoop for MQ : nothing else can be executed in the __init__ after this! Make sure that the self.ready() call is the last line of your init!!!!")
        if ioloopstart == 1:
            IOLoop.instance().start()



    def on_mdp_request(self, msg):
        """ Handle Requests over MQ
            @param msg : MQ req message
        """
        self.log.debug(u"MQ Request received : {0}" . format(str(msg)))

        ### stop the plugin
        if msg.get_action() == "plugin.stop.do":
            self.log.info(u"Plugin stop request : {0}".format(msg))
            self._mdp_reply_plugin_stop(msg)
        elif msg.get_action() == "helper.list.get":
            self.log.info(u"Plugin helper list request : {0}".format(msg))
            self._mdp_reply_helper_list(msg)
        elif msg.get_action() == "helper.help.get":
            self.log.info(u"Plugin helper help request : {0}".format(msg))
            self._mdp_reply_helper_help(msg)
        elif msg.get_action() == "helper.do":
            self.log.info(u"Plugin helper action request : {0}".format(msg))
            self._mdp_reply_helper_do(msg)
        elif msg.get_action() == "device.new.get":
            self.log.info(u"Plugin new devices request : {0}".format(msg))
            self._mdp_reply_device_new_get(msg)
    
    def _mdp_reply_helper_do(self, msg):
        contens = msg.get_data()
        if 'command' in contens.keys():
            if contens['command'] in self.helpers.keys():
                if 'parameters' not in contens.keys():
                    contens['parameters'] = {}
                    params = []
                else:
                    params = []
                    for key, value in contens['parameters'].items():
                        params.append( "{0}='{1}'".format(key, value) )
                command = "self.{0}(".format(self.helpers[contens['command']]['call'])
                command += ", ".join(params)
                command += ")"
                result = eval(command)
                # run the command with all params
                msg = MQMessage()
                msg.set_action('helper.do.result')
                msg.add_data('command', contens['command'])
                msg.add_data('parameters', contens['parameters'])
                msg.add_data('result', result)
                self.reply(msg.get())

    def _mdp_reply_helper_help(self, data):
        content = data.get_data()
        if 'command' in contens.keys():
            if content['command'] in self.helpers.keys():
                msg = MQMessage()
                msg.set_action('helper.help.result')
                msg.add_data('help', self.helpers[content['command']]['help'])
                self.reply(msg.get())

    def _mdp_reply_plugin_stop(self, data):
        """ Stop the plugin
            @param data : MQ req message

            First, send the MQ Rep to 'ack' the request
            Then, change the plugin status to STATUS_STOP_REQUEST
            Then, quit the plugin by calling force_leave(). This should make the plugin send a STATUS_STOPPED if all is ok

            Notice that no check is done on the MQ req content : we need nothing in it as it is directly addressed to a plugin
        """
        # check if the message is for us
        content = data.get_data()
        if content['name'] != self._name or content['host'] != self.get_sanitized_hostname():
            return

        ### Send the ack over MQ Rep
        msg = MQMessage()
        msg.set_action('plugin.stop.result')
        status = True
        reason = ""
        msg.add_data('status', status)
        msg.add_data('reason', reason)
        self.reply(msg.get())

        ### Change the plugin status
        self._set_status(STATUS_STOP_REQUEST)

        ### Try to stop the plugin
        # if it fails, the manager should try to kill the plugin
        self.force_leave()

    def _mdp_reply_helper_list(self, data):
        """ Return a list of supported helpers
            @param data : MQ req message
        """
        ### Send the ack over MQ Rep
        msg = MQMessage()
        msg.set_action('helper.list.result')
        msg.add_data('actions', self.helpers.keys())
        self.reply(msg.get())

    def _mdp_reply_device_new_get(self, data):
        """ Return a list of new devices detected
            @param data : MQ req message
        """
        ### Send the ack over MQ Rep
        msg = MQMessage()
        msg.set_action('device.new.result')
        msg.add_data('devices', self.new_devices)
        self.reply(msg.get())


    def _set_status(self, status):
        """ Set the plugin status and send it
        """
        # when ctrl-c is done, there is no more self._name at this point...
        # why ? because de force_leave method is called twice as show in the logs : 
        #
        # ^CKeyBoardInterrupt
        # 2013-12-20 22:48:41,040 domogik-manager INFO Keyoard Interrupt detected, leave now.
        # Traceback (most recent call last):
        #   File "./manager.py", line 1176, in <module>
        #     main()
        #   File "./manager.py", line 1173, in main
        # 2013-12-20 22:48:41,041 domogik-manager DEBUG force_leave called
        # 2013-12-20 22:48:41,044 domogik-manager DEBUG __del__ Single xpl plugin
        # 2013-12-20 22:48:41,045 domogik-manager DEBUG force_leave called

        if hasattr(self, '_name'):
            if self._name not in CORE_COMPONENTS:
                self._status = status
                self._send_status()

    def _send_status_loop(self):
        """ send the status each STATUS_HBEAT seconds
        """
        # TODO : we could optimize by resetting the timer each time the status is sent
        # but as this is used only to check for dead plugins by the manager, it is not very important ;)
        while not self._stop.isSet():
            # TODO : remove
            self.log.debug("SEND STATUS LOOP")
            self._send_status()
            self._stop.wait(STATUS_HBEAT)

    def _send_status(self):
        """ Send the plugin status over the MQ
        """ 
        if hasattr(self, "_pub"):
            if self._name in CORE_COMPONENTS:
                #type = "core"
                return
            else:
                type = "plugin"
            self.log.debug("Send plugin status : {0}".format(self._status))
            self._pub.send_event('plugin.status', 
                                 {"type" : type,
                                  "name" : self._name,
                                  "host" : self.get_sanitized_hostname(),
                                  "event" : self._status})

    def get_config_files(self):
       """ Return list of config files
       """
       return self._config_files

    def get_products_directory(self):
       """ getter 
       """
       return self.products_directory

    def get_libraries_directory(self):
       """ getter 
       """
       return self.libraries_directory

    def get_packages_directory(self):
       """ getter 
       """
       return self.packages_directory

    def get_resources_directory(self):
       """ getter 
       """
       return self.resources_directory

    def get_data_files_directory(self):
       """
       Return the directory where a plugin developper can store data files.
       If the directory doesn't exist, try to create it.
       After that, try to create a file inside it.
       If something goes wrong, generate an explicit exception.
       """
       cfg = Loader('domogik')
       my_conf = cfg.load()
       config = dict(my_conf[1])
       path = "{0}/{1}/{2}_{3}/data/" % (self.librairies_directory, PACKAGES_DIR, "plugin", self._name)
       if os.path.exists(path):
           if not os.access(path, os.W_OK & os.X_OK):
               raise OSError("Can't write in directory %s" % path)
       else:
           try:
               os.mkdir(path, '0770')
               self.log.info(u"Create directory %s." % path)
           except:
               raise OSError("Can't create directory %s." % path)
       try:
           tmp_prefix = "write_test";
           count = 0
           filename = os.path.join(path, tmp_prefix)
           while(os.path.exists(filename)):
               filename = "{}.{}".format(os.path.join(path, tmp_prefix),count)
               count = count + 1
           f = open(filename,"w")
           f.close()
           os.remove(filename)
       except :
           raise IOError("Can't create a file in directory %s." % path)
       return path

    def register_helper(self, action, help_string, callback):
        if action not in self.helpers:
            self.helpers[action] = {'call': callback, 'help': help_string}

    def publish_helper(self, key, data):
        if hasattr(self, "_pub"):
            if self._name in CORE_COMPONENTS:
                type = "core"
            else:
                type = "plugin"
            self._pub.send_event('helper.publish',
                                 {"origin" : self._mq_name,
                                  "key": key,
                                  "data": data})

    # TODO :remove
    #def get_stats_files_directory(self):
    #   """ Return the directory where a plugin developper can store data files
    #   """
    #   cfg = Loader('domogik')
    #   my_conf = cfg.load()
    #   config = dict(my_conf[1])
    #   if config.has_key('package_path'):
    #       path = "%s/domogik_packages/stats/%s" % (config['package_path'], self._name)
    #   else:
    #       path = "%s/share/domogik/stats/%s" % (config['src_prefix'], self._name)
    #   return path

    def enable_hbeat(self, lock = False):
        """ Wrapper for xplconnector.enable_hbeat()
        """
        self.myxpl.enable_hbeat(lock)
        self.enable_hbeat_called = True

    def _send_process_info(self, pid, data):
        """ Send process info (cpu, memory) on xpl
            @param : process pid
            @param data : dictionnary of process informations
        """
        mess = XplMessage()
        mess.set_type("xpl-stat")
        mess.set_schema("domogik.usage")
        mess.add_data({"name" : "%s.%s" % (self.get_plugin_name(), self.get_sanitized_hostname()),
                       "pid" : pid,
                       "cpu-percent" : data["cpu_percent"],
                       "memory-percent" : data["memory_percent"],
                       "memory-rss" : data["memory_rss"],
                       "memory-vsz" : data["memory_vsz"]})
        self.myxpl.send(mess)

    def _get_pid(self):
        """ Get current pid and write it to a file
        """
        pid = os.getpid()
        pid_file = os.path.join(self._pid_dir_path,
                                self._name + ".pid")
        self.log.debug(u"Write pid file for pid '%s' in file '%s'" % (str(pid), pid_file))
        fil = open(pid_file, "w")
        fil.write(str(pid))
        fil.close()

    def __del__(self):
        if hasattr(self, "log"):
            self.log.debug(u"__del__ Single xpl plugin")
            # we guess that if no "log" is defined, the plugin has not really started, so there is no need to call force leave (and _stop, .... won't be created)
            self.force_leave()

    def _send_hbeat_end(self):
        """ Send the hbeat.end message
        """
        if hasattr(self, "myxpl"):
            mess = XplMessage()
            mess.set_type("xpl-stat")
            mess.set_schema("hbeat.end")
            self.myxpl.send(mess)

    def _manager_handler(self, message):
        """ Handle domogik system request for the Domogik manager
        @param message : the Xpl message received
        """

    def wait(self):
        """ Wait until someone ask the plugin to stop
        """
        self.myxpl._network.join()

    def force_leave(self, status = False, return_code = None):
        """ Leave threads & timers
        """
        if return_code != None:
            self.set_return_code(return_code)
            self.log.info("Return code set to {0} when calling force_leave()".format(return_code))


        # avoid ready() to be launched
        self.dont_run_ready = True
        # stop IOLoop
        #try:
        #    IOLoop.instance().start()
        #except:
        #    pass
        if hasattr(self, "log"):
            self.log.debug(u"force_leave called")
        # send stopped status over the MQ
        if status:
            self._set_status(status)
        else:
            self._set_status(STATUS_STOPPED)

        # send hbeat.end message
        self._send_hbeat_end()

        # try to stop the thread
        try:
            self.get_stop().set()
        except AttributeError:
            pass

        if hasattr(self, "_timers"):
            for t in self._timers:
                if hasattr(self, "log"):
                    self.log.debug(u"Try to stop timer %s"  % t)
                t.stop()
                if hasattr(self, "log"):
                    self.log.debug(u"Timer stopped %s" % t)

        if hasattr(self, "_stop_cb"):
            for cb in self._stop_cb:
                if hasattr(self, "log"):
                    self.log.debug(u"Calling stop additionnal method : %s " % cb.__name__)
                cb()
    
        if hasattr(self, "_threads"):
            for t in self._threads:
                if hasattr(self, "log"):
                    self.log.debug(u"Try to stop thread %s" % t)
                try:
                    t.join()
                except RuntimeError:
                    pass
                if hasattr(self, "log"):
                    self.log.debug(u"Thread stopped %s" % t)
                #t._Thread__stop()

        #Finally, we try to delete all remaining threads
        for t in threading.enumerate():
            if t != threading.current_thread() and t.__class__ != threading._MainThread:
                if hasattr(self, "log"):
                    self.log.info(u"The thread %s was not registered, killing it" % t.name)
                t.join()
                if hasattr(self, "log"):
                    self.log.info(u"Thread %s stopped." % t.name)

        if threading.activeCount() > 1:
            if hasattr(self, "log"):
                self.log.warn(u"There are more than 1 thread remaining : %s" % threading.enumerate())