class XplManager(XplPlugin, MQAsyncSub): """ Statistics manager """ def __init__(self): """ Initiate DbHelper, Logs and config """ XplPlugin.__init__(self, 'xplgw', log_prefix="") MQAsyncSub.__init__(\ self, self.zmq, 'xplgw', \ ['client.conversion', 'client.list']) self.log.info(u"XPL manager initialisation...") self._db = DbHelper() self.pub = MQPub(zmq.Context(), 'xplgw') # some initial data sets self.client_xpl_map = {} self.client_conversion_map = {} self._db_sensors = {} self._db_xplstats = {} # queue to store the message that needs to be ahndled for sensor checking self._sensor_queue = Queue.Queue() # all command handling params # _lock => to be sure to be thread safe # _dict => uuid to xplstat translationg # _pkt => received messages to check self._cmd_lock_d = threading.Lock() self._cmd_dict = {} self._cmd_lock_p = threading.Lock() self._cmd_pkt = {} # load some initial data from manager and db self._load_client_to_xpl_target() self._load_conversions() # create a general listener self._create_xpl_trigger() # start handling the xplmessages self._s_thread = self._SensorThread(\ self.log, self._sensor_queue, \ self.client_conversion_map, self.pub) self._s_thread.start() # start handling the command reponses in a thread self._c_thread = self._CommandThread(\ self.log, self._db, self._cmd_lock_d, \ self._cmd_lock_p, self._cmd_dict, self._cmd_pkt, self.pub) self._c_thread.start() # start the sensorthread self.ready() def on_mdp_request(self, msg): """ Method called when an mq request comes in XplPlugin also needs this info, so we need to do a passthrough """ try: XplPlugin.on_mdp_request(self, msg) if msg.get_action() == "test": pass if msg.get_action() == "cmd.send": self._send_xpl_command(msg) except Exception as exp: self.log.error(traceback.format_exc()) def on_message(self, msgid, content): """ Method called on a subscribed message """ try: if msgid == 'client.conversion': self._parse_conversions(content) elif msgid == 'client.list': self._parse_xpl_target(content) except Exception as exp: self.log.error(traceback.format_exc()) def _load_client_to_xpl_target(self): """ Request the client conversion info This is an mq req to manager """ cli = MQSyncReq(self.zmq) msg = MQMessage() msg.set_action('client.list.get') response = cli.request('manager', msg.get(), timeout=10) if response: self._parse_xpl_target(response.get_data()) else: self.log.error(\ u"Updating client list failed, no response from manager") def _parse_xpl_target(self, data): """ Translate the mq data info a dict for the xpl targets """ tmp = {} for cli in data: tmp[cli] = data[cli]['xpl_source'] self.client_xpl_map = tmp def _load_conversions(self): """ Request the client conversion info This is an mq req to manager """ cli = MQSyncReq(self.zmq) msg = MQMessage() msg.set_action('client.conversion.get') response = cli.request('manager', msg.get(), timeout=10) if response: self._parse_conversions(response.get_data()) else: self.log.error(\ u"Updating conversion list failed, no response from manager") def _parse_conversions(self, data): """ Translate the mq data into a dict """ tmp = {} for cli in data: tmp[cli] = data[cli] self.client_conversion_map = tmp def _send_xpl_command(self, data): """ Reply to config.get MQ req @param data : MQ req message Needed info in data: - cmdid => command id to send - cmdparams => key/value pair of all params needed for this command """ with self._db.session_scope(): self.log.info(u"Received new cmd request: {0}".format(data)) failed = False request = data.get_data() if 'cmdid' not in request: failed = "cmdid not in message data" if 'cmdparams' not in request: failed = "cmdparams not in message data" if not failed: # get the command cmd = self._db.get_command(request['cmdid']) if cmd is not None: if cmd.xpl_command is not None: xplcmd = cmd.xpl_command xplstat = self._db.get_xpl_stat(xplcmd.stat_id) if xplstat is not None: # get the device from the db dev = self._db.get_device(int(cmd.device_id)) msg = XplMessage() if not dev['client_id'] in self.client_xpl_map.keys(): self._load_client_to_xpl_target() if not dev['client_id'] in self.client_xpl_map.keys(): failed = "Can not fincd xpl source for {0} client_id".format(dev['client_id']) else: msg.set_target(self.client_xpl_map[dev['client_id']]) msg.set_source(self.myxpl.get_source()) msg.set_type("xpl-cmnd") msg.set_schema(xplcmd.schema) # static paramsw for par in xplcmd.params: msg.add_data({par.key : par.value}) # dynamic params for par in cmd.params: if par.key in request['cmdparams']: value = request['cmdparams'][par.key] # chieck if we need a conversion if par.conversion is not None and par.conversion != '': if dev['client_id'] in self.client_conversion_map: if par.conversion in self.client_conversion_map[dev['client_id']]: exec(self.client_conversion_map[dev['client_id']][par.conversion]) value = locals()[par.conversion](value) msg.add_data({par.key : value}) else: failed = "Parameter ({0}) for device command msg is not provided in the mq message".format(par.key) if not failed: # send out the msg self.log.debug(u"Sending xplmessage: {0}".format(msg)) self.myxpl.send(msg) xplstat = self._db.detach(xplstat) # generate an uuid for the matching answer published messages if xplstat != None: resp_uuid = uuid4() self._cmd_lock_d.acquire() self._cmd_dict[str(resp_uuid)] = xplstat self._cmd_lock_d.release() else: resp_uuid = None # send the response reply_msg = MQMessage() reply_msg.set_action('cmd.send.result') reply_msg.add_data('uuid', str(resp_uuid)) reply_msg.add_data('status', True) reply_msg.add_data('reason', None) self.log.debug(u"mq reply (success) : {0}".format(reply_msg.get())) self.reply(reply_msg.get()) if failed: self.log.error(failed) reply_msg = MQMessage() reply_msg.set_action('cmd.send.result') reply_msg.add_data('uuid', None) reply_msg.add_data('status', False) reply_msg.add_data('reason', failed) self.log.debug(u"mq reply (failed) : {0}".format(reply_msg.get())) self.reply(reply_msg.get()) def _create_xpl_trigger(self): """ Create a listener to catch all xpl-stats and xpl-trig messages """ Listener(self._xpl_callback, self.myxpl, {'xpltype': 'xpl-trig'}) Listener(self._xpl_callback, self.myxpl, {'xpltype': 'xpl-stat'}) def _xpl_callback(self, pkt): """ The callback for the xpl messages push them into the needed queues """ item = {} item["msg"] = pkt item["clientId"] = next((cli for cli, xpl in self.client_xpl_map.items() if xpl == pkt.source), None) self._sensor_queue.put(item) self.log.debug(u"Adding new message to the sensorQueue, current length = {0}".format(self._sensor_queue.qsize())) #self.log.debug(u"Adding new message to the sensorQueue, current length = {0}, message = {1}".format(self._sensor_queue.qsize(), pkt)) self._cmd_lock_p.acquire() # only do this when we have outstanding commands if len(self._cmd_dict) > 0: self._cmd_pkt[time.time()] = pkt self.log.debug(u"Adding new message to the cmdQueue, current length = {0}".format(len(self._cmd_dict))) #self.log.debug(u"Adding new message to the cmdQueue, current length = {0}, message = {1}".format(len(self._cmd_dict), pkt)) self._cmd_lock_p.release() class _CommandThread(threading.Thread): """ commandthread class Class responsible for handling one xpl command """ def __init__(self, log, db, lock_d, lock_p, dic, pkt, pub): threading.Thread.__init__(self) self._db = DbHelper() self._log = log self._lock_d = lock_d self._lock_p = lock_p self._dict = dic self._pkt = pkt self._pub = pub def run(self): while True: # remove old pkts self._lock_p.acquire() for pkt in self._pkt.keys(): if pkt < time.time() - CMDTIMEOUT: self._log.warning(u"Delete packet too old (timeout reached) : {0}".format(pkt)) del(self._pkt[pkt]) self._lock_p.release() # now try to match if we have enough data if len(self._dict) > 0 and len(self._pkt) > 0: todel_pkt = [] todel_dict = [] for uuid, search in self._dict.items(): for tim, pkt in self._pkt.items(): if search.schema == pkt.schema: found = True for par in search.params: if par.key not in pkt.data: if par.value != pkt.data[par.key]: found = False elif par.multiple is not None and len(par.multiple) == 1: if pkt.data[par.key] not in par.value.split(par.multiple): found = False if found: self._log.info(u"Found response message to command with uuid: {0}".format(uuid)) # publish the result self._pub.send_event('command.result', \ {"uuid" : uuid}) todel_pkt.append(tim) todel_dict.append(uuid) # now go and delete the unneeded data self._lock_p.acquire() for tim in todel_pkt: if tim in self._pkt: del(self._pkt[tim]) #self._log.debug(u"Deleting message from the cmdQueue, current length = {0}".format(len(self._pkt))) # TODO : remove or comment the 2 following lines #self._log.debug(u"Data to delete : {0}".format(todel_dict)) #self._log.debug(u"Content before deletion : {0}".format(self._dict)) self._lock_p.release() self._lock_d.acquire() for tim in todel_dict: if tim in self._dict: del(self._dict[tim]) self._lock_d.release() todel_pkt = [] todel_dict = [] else: # nothing todo, sleep a second time.sleep(1) class _SensorThread(threading.Thread): """ SensorThread class Class that will handle the sensor storage in a seperated thread This will get messages from the SensorQueue """ def __init__(self, log, queue, conv, pub): threading.Thread.__init__(self) self._db = DbHelper() self._log = log self._queue = queue self._conv = conv self._pub = pub def _find_storeparam(self, item): #print("ITEM = {0}".format(item['msg'])) found = False tostore = [] for xplstat in self._db.get_all_xpl_stat(): sensors = 0 matching = 0 statics = 0 if xplstat.schema == item["msg"].schema: #print(" XPLSTAT = {0}".format(xplstat)) # we found a possible xplstat # try to match all params and try to find a sensorid and a vlaue to store for param in xplstat.params: #print(" PARAM = {0}".format(param)) ### Caution ! # in case you, who are reading this, have to debug something like that : # 2015-08-16 22:04:26,190 domogik-xplgw INFO Storing stat for device 'Garage' (6) and sensor 'Humidity' (69): key 'current' with value '53' after conversion. # 2015-08-16 22:04:26,306 domogik-xplgw INFO Storing stat for device 'Salon' (10) and sensor 'Humidity' (76): key 'current' with value '53' after conversion. # 2015-08-16 22:04:26,420 domogik-xplgw INFO Storing stat for device 'Chambre d'Ewan' (11) and sensor 'Humidity' (80): key 'current' with value '53' after conversion. # 2015-08-16 22:04:26,533 domogik-xplgw INFO Storing stat for device 'Chambre des parents' (12) and sensor 'Humidity' (84): key 'current' with value '53' after conversion. # 2015-08-16 22:04:26,651 domogik-xplgw INFO Storing stat for device 'Chambre de Laly' (13) and sensor 'Humidity' (88): key 'current' with value '53' after conversion. # 2015-08-16 22:04:26,770 domogik-xplgw INFO Storing stat for device 'Entrée' (17) and sensor 'Humidity' (133): key 'current' with value '53' after conversion. # # which means that for a single xPL message, the value is stored in several sensors (WTF!!! ?) # It can be related to the fact that the device address key is no more corresponding between the plugin (info.json and xpl sent by python) and the way the device was create in the databse # this should not happen, but in case... well, we may try to find a fix... if param.key in item["msg"].data and param.static: statics = statics + 1 if param.multiple is not None and len(param.multiple) == 1 and item["msg"].data[param.key] in param.value.split(param.multiple): matching = matching + 1 elif item["msg"].data[param.key] == param.value: matching = matching + 1 # now we have a matching xplstat, go and find all sensors if matching == statics: #print(" MATHING !!!!!") for param in xplstat.params: if param.key in item["msg"].data and not param.static: #print(" ===> TOSTORE !!!!!!!!! : {0}".format({'param': param, 'value': item["msg"].data[param.key]})) tostore.append( {'param': param, 'value': item["msg"].data[param.key]} ) if len(tostore) > 0: found = True if found: return (found, tostore) else: return False def run(self): while True: try: item = self._queue.get() self._log.debug(u"Getting item from the sensorQueue, current length = {0}".format(self._queue.qsize())) # if clientid is none, we don't know this sender so ignore # TODO check temp disabled until external members are working #if item["clientId"] is not None: if True: with self._db.session_scope(): fdata = self._find_storeparam(item) if fdata: #// ICI !!! self._log.debug(u"Found a matching sensor, so starting the storage procedure. Sensor : {0}".format(fdata)) for data in fdata[1]: value = data['value'] storeparam = data['param'] current_date = calendar.timegm(time.gmtime()) store = True if storeparam.ignore_values: if value in eval(storeparam.ignore_values): self._log.debug(u"Value {0} is in the ignore list {0}, so not storing.".format(value, storeparam.ignore_values)) store = False if store: # get the sensor and dev sen = self._db.get_sensor(storeparam.sensor_id) dev = self._db.get_device(sen.device_id) # check if we need a conversion if sen.conversion is not None and sen.conversion != '': if dev['client_id'] in self._conv and sen.conversion in self._conv[dev['client_id']]: self._log.debug( \ u"Calling conversion {0}".format(sen.conversion)) exec(self._conv[dev['client_id']][sen.conversion]) value = locals()[sen.conversion](value) self._log.info( \ u"Storing stat for device '{0}' ({1}) and sensor '{2}' ({3}): key '{4}' with value '{5}' after conversion." \ .format(dev['name'], dev['id'], sen.name, sen.id, storeparam.key, value)) # do the store try: self._db.add_sensor_history(\ storeparam.sensor_id, \ value, \ current_date) except Exception as exp: self._log.error(u"Error when adding sensor history : {0}".format(traceback.format_exc())) else: self._log.debug(u"Don't need to store this value") # publish the result self._pub.send_event('device-stats', \ {"timestamp" : current_date, \ "device_id" : dev['id'], \ "sensor_id" : sen.id, \ "stored_value" : value}) except Queue.Empty: # nothing in the queue, sleep for 1 second time.sleep(1) except Exception as exp: self._log.error(traceback.format_exc())