def on_init(self):
     # module's configuration
     self.config = {}
     # map rule_id with rule configuration
     self.rules = {}
     # map rule_id with an array of request_id the rule will wait for to complete
     self.requests = {}
     # map for each rule_id constants/variable_id with their values
     self.values = {}
     # for manually run rules, map rule_id with requesting message
     self.on_demand = {}
     # for rule without a schedule, map sensor_id with an array of rules to run upon a change
     self.triggers = {}
     # map rule_id with the timestamp it run last so to avoid running it too frequently
     self.last_run = {}
     # map rule_id with scheduler job_id
     self.jobs = {}
     # map sendor_id with sensor configuration
     self.sensors = {}
     # map for each rule_id/variable, the associated sensor_id
     self.variables = {}
     # map for each rule_id the number of notifications sent by hour
     self.notifications = {}
     # scheduler is needed for scheduling rules
     self.scheduler = Scheduler(self)
     # regular expression used to parse variables
     self.variable_regexp = '^(DISTANCE|TIMESTAMP|ELAPSED|COUNT|SCHEDULE|POSITION_LABEL|POSITION_TEXT|)\s*(-\d+)?(,-\d+)?\s*(\S+)$'
     # require module configuration before starting up
     self.config_schema = 2
     self.rules_config_schema = 2
     self.sensors_config_schema = 1
     self.add_configuration_listener(self.fullname, "+", True)
     # subscribe for acknowledgments from the database for saved values
     self.add_inspection_listener("controller/db", "*/*", "SAVED", "#")
Esempio n. 2
0
 def __init__(self, scope, name):
     # call superclass function
     super(Service, self).__init__(scope, name)
     # initialize internal cache
     self.cache = Cache()
     # scheduler is needed for polling sensors
     self.__scheduler = Scheduler(self)
     # map sensor_id with scheduler job_id
     self.__jobs = {}
     # map sensor_id with service's configuration
     self.sensors = {}
     # for pull services, if polling at startup
     self.poll_at_startup = bool(
         int(os.getenv("EGEOFFREY_POLL_SENSORS_AT_STARTUP", True)))
Esempio n. 3
0
class Service(Module):
    # What to do when initializing
    def __init__(self, scope, name):
        # call superclass function
        super(Service, self).__init__(scope, name)
        # initialize internal cache
        self.cache = Cache()
        # scheduler is needed for polling sensors
        self.__scheduler = Scheduler(self)
        # map sensor_id with scheduler job_id
        self.__jobs = {}
        # map sensor_id with service's configuration
        self.sensors = {}
        # for pull services, if polling at startup
        self.poll_at_startup = bool(
            int(os.getenv("EGEOFFREY_POLL_SENSORS_AT_STARTUP", True)))

    # function to run when scheduling a job
    def __poll_sensor(self, sensor_id, configuration):
        # simulate a message from the hub to trigger the sensor
        message = Message(self)
        message.sender = "controller/hub"
        message.recipient = self.fullname
        message.command = "IN"
        message.args = sensor_id
        message.set_data(configuration)
        self.on_message(message)

    # unschedule a job
    def __remove_schedule(self, sensor_id):
        # if already scheduled, stop it
        if sensor_id in self.__jobs:
            try:
                for job in self.__scheduler.get_jobs():
                    if job.id == self.__jobs[sensor_id]:
                        self.__scheduler.remove_job(self.__jobs[sensor_id])
            except Exception, e:
                self.log_error("Unable to remove scheduled job for sensor " +
                               sensor_id + ": " + exception.get(e))
            del self.__jobs[sensor_id]
Esempio n. 4
0
 def on_init(self):
     # module's configuration
     self.config = {}
     # logger
     self.logger = logging.getLogger("eGeoffrey")
     # maximum log messages per second to print
     self.max_msg_rate = 5
     self.msg_count = 0
     self.msg_time = time.time()
     # message queue
     self.queue = collections.deque(maxlen=50)
     self.is_logging = False
     # scheduler is needed for purging old logs
     self.scheduler = Scheduler(self)
     # require module configuration before starting up
     self.config_schema = 2
     self.add_configuration_listener(self.fullname, "+", True)
Esempio n. 5
0
 def on_init(self):
     # variables
     self.config_dir = os.getenv(
         "EGEOFFREY_CONFIG_DIR",
         os.path.abspath(os.path.dirname(__file__)) + "/../config")
     self.log_debug("Configuration directory set to " + self.config_dir)
     self.force_reload = int(os.getenv("EGEOFFREY_CONFIG_FORCE_RELOAD", 0))
     self.accept_default_config = int(
         os.getenv("EGEOFFREY_CONFIG_ACCEPT_DEFAULTS", 1))
     # keep track of the old config index
     self.old_index = None
     self.index_key = "__index"
     self.index_version = 1
     self.supported_manifest_schema = 2
     # scheduler is used for scheduling config reload
     self.scheduler = Scheduler(self)
     # status flags
     self.load_config_running = False
     self.clear_config_running = False
     self.is_config_online = False
     self.reload_scheduled = False
class Alerter(Controller):
    # What to do when initializing
    def on_init(self):
        # module's configuration
        self.config = {}
        # map rule_id with rule configuration
        self.rules = {}
        # map rule_id with an array of request_id the rule will wait for to complete
        self.requests = {}
        # map for each rule_id constants/variable_id with their values
        self.values = {}
        # for manually run rules, map rule_id with requesting message
        self.on_demand = {}
        # for rule without a schedule, map sensor_id with an array of rules to run upon a change
        self.triggers = {}
        # map rule_id with the timestamp it run last so to avoid running it too frequently
        self.last_run = {}
        # map rule_id with scheduler job_id
        self.jobs = {}
        # map sendor_id with sensor configuration
        self.sensors = {}
        # map for each rule_id/variable, the associated sensor_id
        self.variables = {}
        # map for each rule_id the number of notifications sent by hour
        self.notifications = {}
        # scheduler is needed for scheduling rules
        self.scheduler = Scheduler(self)
        # regular expression used to parse variables
        self.variable_regexp = '^(DISTANCE|TIMESTAMP|ELAPSED|COUNT|SCHEDULE|POSITION_LABEL|POSITION_TEXT|)\s*(-\d+)?(,-\d+)?\s*(\S+)$'
        # require module configuration before starting up
        self.config_schema = 2
        self.rules_config_schema = 2
        self.sensors_config_schema = 1
        self.add_configuration_listener(self.fullname, "+", True)
        # subscribe for acknowledgments from the database for saved values
        self.add_inspection_listener("controller/db", "*/*", "SAVED", "#")

    # calculate a sub expression
    def evaluate_condition(self, a, operator, b):
        # prepare the values (can be an array)
        if isinstance(a, list): a = a[0]
        if isinstance(b, list): b = b[0]
        # perform integrity checks
        if a is None or b is None: return None
        if not sdk.python.utils.numbers.is_number(
                a) or not sdk.python.utils.numbers.is_number(b):
            return None
        # calculate the expression
        if operator == "+": return float(a) + float(b)
        elif operator == "-": return float(a) - float(b)
        elif operator == "*": return float(a) * float(b)
        elif operator == "/": return float(a) / float(b)
        return None

    # evaluate if a condition is met
    def is_true(self, a, operator, b):
        evaluation = True
        # get a's value
        if not isinstance(a, list): a = [a]
        if len(a) == 0: return False
        a = a[0]
        # prepare b's value
        if not isinstance(b, list): b = [b]
        if len(b) == 0: return False
        # b can be have multiple values, cycle through all of them
        for value in b:
            if value is None or a is None: evaluation = False
            elif operator == "==":
                if value != a: evaluation = False
            elif operator == "!=":
                if value == a: evaluation = False
            elif operator == ">":
                if not sdk.python.utils.numbers.is_number(
                        value) or not sdk.python.utils.numbers.is_number(a):
                    return False
                if float(value) >= float(a): evaluation = False
            elif operator == "<":
                if not sdk.python.utils.numbers.is_number(
                        value) or not sdk.python.utils.numbers.is_number(a):
                    return False
                if float(value) <= float(a): evaluation = False
            elif operator == "in":
                evaluation = str(a) in str(value)
            else:
                evaluation = False
        # return the evaluation
        return evaluation

    # replace placeholders (%placeholder%) with their values
    def format_placeholders(self, rule_id, macro, text):
        # find all &placeholder%
        placeholders = re.findall("%([^%]+)%", text)
        for placeholder in placeholders:
            # replace the macro with its name
            if placeholder == "i":
                macro_text = macro
                if macro in self.sensors:
                    sensor = self.sensors[macro]
                    if "description" in sensor:
                        macro_text = sensor["description"]
                text = text.replace("%i%", macro_text)
            else:
                # get the value of the placeholder
                if isinstance(self.values[rule_id][macro][placeholder], list):
                    if len(self.values[rule_id][macro][placeholder]) == 0:
                        value = ""
                    else:
                        value = self.values[rule_id][macro][placeholder][0]
                else:
                    value = self.values[rule_id][macro][placeholder]
                # append the unit to the value if we got this sensor's configuration
                if rule_id in self.variables and macro in self.variables[
                        rule_id] and placeholder in self.variables[rule_id][
                            macro]:
                    sensor_id = self.variables[rule_id][macro][placeholder]
                    if sensor_id in self.sensors and "unit" in self.sensors[
                            sensor_id]:
                        value = str(value) + str(
                            self.sensors[sensor_id]["unit"])
                text = text.replace("%" + placeholder + "%", str(value))
        return text

    # retrieve the values of all the configured variables of the given rule_id. Continues in on_messages() when receiving values from db
    def run_rule(self, rule_id, macro=None):
        # if macro is defined, run the rule for that macro only, otherwise run for all the macros
        macros = [macro] if macro is not None else self.rules[rule_id].keys()
        for macro in macros:
            rule = self.rules[rule_id][macro]
            # ensure this rule is not run too often to avoid loops
            if self.config[
                    "loop_safeguard"] > 0 and rule_id in self.last_run and macro in self.last_run[
                        rule_id] and time.time() - self.last_run[rule_id][
                            macro] < self.config["loop_safeguard"]:
                return
            # keep track of the last time this run has run
            if rule_id not in self.last_run: self.last_run[rule_id] = {}
            self.last_run[rule_id][macro] = time.time()
            self.log_debug("[" + rule_id + "][" + macro + "] running rule")
            # for each sensor we need the value which will be asked to the db module. Keep track of both values and requests
            if rule_id not in self.requests: self.requests[rule_id] = {}
            self.requests[rule_id][macro] = []
            if rule_id not in self.values: self.values[rule_id] = {}
            self.values[rule_id][macro] = {}
            # for every constant, store its value as is so will be ready for the evaluation
            if "constants" in rule:
                for constant_id, value in rule["constants"].iteritems():
                    self.values[rule_id][macro][constant_id] = value
            # for every variable, retrieve its latest value to the database
            if "variables" in rule:
                for variable_id, variable in rule["variables"].iteritems():
                    # process the variable string (0: request, 1: start, 2: end, 3: sensor_id)
                    match = re.match(self.variable_regexp, variable)
                    if match is None: continue
                    # query db for the data
                    command, start, end, sensor_id = match.groups()
                    message = Message(self)
                    message.recipient = "controller/db"
                    message.command = message.command = "GET_" + command if command != "" else "GET"
                    message.args = sensor_id
                    start = -1 if start is None else int(start)
                    end = -1 if end is None else int(end.replace(",", ""))
                    message.set("start", start)
                    message.set("end", end)
                    self.sessions.register(
                        message, {
                            "rule_id": rule_id,
                            "variable_id": variable_id,
                            "macro": macro,
                        })
                    self.log_debug("[" + rule_id + "][" + macro + "][" +
                                   variable_id + "] requesting db for " +
                                   message.command + " " + message.args +
                                   ": " + str(message.get_data()))
                    self.send(message)
                    # keep track of the requests so that once all the data will be available the rule will be evaluated
                    self.requests[rule_id][macro].append(
                        message.get_request_id())
                # add a placeholder at the end to ensure the rule is not evaluated before all the definitions are retrieved
                self.requests[rule_id][macro].append("LAST")
            # if the rule requires no data to retrieve, just evaluate it
            if len(self.requests[rule_id][macro]) == 0:
                self.evaluate_rule(rule_id, macro)

    # evaluate the conditions of a rule, once all the variables have been collected
    def evaluate_rule(self, rule_id, macro):
        rule = self.rules[rule_id][macro]
        # 1) evaluate all the conditions of the rule
        or_evaluations = []
        for or_conditions in rule["conditions"]:
            and_evaluations = []
            for and_conditions in or_conditions:
                # remove spaces
                and_conditions = re.sub(' +', ' ', and_conditions)
                # look for sub expressions (grouped within parenthesis) and calculate them individually
                expressions = re.findall("\(([^\)]+)\)", and_conditions)
                for i in range(len(expressions)):
                    expression = expressions[i]
                    # subexpression will become internal variables
                    placeholder = "%exp_" + str(i) + "%"
                    # expression format is "exp1 operator exp2" (e.g. a == b)
                    exp1, operator, exp2 = expression.split(' ')
                    # calculate the sub expression
                    exp1_value = self.values[rule_id][macro][exp1]
                    exp2_value = self.values[rule_id][macro][exp2]
                    exp_value = self.evaluate_condition(
                        exp1_value, operator, exp2_value)
                    self.log_debug("[" + rule_id + "][" + macro +
                                   "] resolving " + exp1 + " (" +
                                   sdk.python.utils.strings.truncate(
                                       str(exp1_value), 50) + ") " + operator +
                                   " " + exp2 + " (" +
                                   sdk.python.utils.strings.truncate(
                                       str(exp2_value), 50) + "): " +
                                   str(exp_value) + " (alias " + placeholder +
                                   ")")
                    # store the sub expressions result in the values
                    self.values[rule_id][macro][placeholder] = exp_value
                    and_conditions = and_conditions.replace(
                        "(" + expression + ")", placeholder)
                # evaluate the main expression
                a, operator, b = and_conditions.split(' ')
                a_value = self.values[rule_id][macro][a]
                b_value = self.values[rule_id][macro][b]
                sub_evaluation = self.is_true(a_value, operator, b_value)
                self.log_debug(
                    "[" + rule_id + "][" + macro + "] evaluating condition " +
                    a + " (" +
                    sdk.python.utils.strings.truncate(str(a_value), 50) +
                    ") " + operator + " " + b + " (" +
                    sdk.python.utils.strings.truncate(str(b_value), 50) +
                    "): " + str(sub_evaluation))
                and_evaluations.append(sub_evaluation)
            # evaluation is true if all the conditions are met
            and_evaluation = True
            for evaluation in and_evaluations:
                if not evaluation: and_evaluation = False
            self.log_debug("[" + rule_id + "][" + macro +
                           "] AND block evaluates to " + str(and_evaluation))
            or_evaluations.append(and_evaluation)
        # evaluation is true if at least one condition is met
        or_evaluation = False
        for evaluation in or_evaluations:
            if evaluation: or_evaluation = True
        # if there were no conditions, the rule evaluates to true
        if len(or_evaluations) == 0: or_evaluation = True
        self.log_debug("[" + rule_id + "][" + macro + "] rule evaluates to " +
                       str(or_evaluation))
        # evaluate to false, just return
        if not or_evaluation:
            return
        # if suppress is in place for this rule, just return
        if self.filter_notification(rule_id, rule):
            return
        # 2) execute the requested actions
        if "actions" in rule:
            for action in rule["actions"]:
                action = re.sub(' +', ' ', action)
                # replace constants and variables placeholders in the action with their values
                action = self.format_placeholders(rule_id, macro, action)
                # execute the action
                action_split = action.split(" ")
                command = action_split[0]
                # set the sensor to a value or poll it
                if command == "SET" or command == "POLL":
                    sensor_id = action_split[1]
                    message = Message(self)
                    message.recipient = "controller/hub"
                    message.command = command
                    message.args = sensor_id
                    if command == "SET": message.set_data(action_split[2])
                    self.send(message)
                # run another rule
                elif command == "RUN":
                    rule_to_run = action_split[1]
                    message = Message(self)
                    message.recipient = "controller/alerter"
                    message.command = command
                    message.args = rule_to_run
                    self.send(message)
        # 3) format the alert text
        # replace constants and variables placeholders in the alert text with their values
        alert_text = self.format_placeholders(rule_id, macro, rule["text"])
        # 4) notify about the alert and save it
        if rule["severity"] != "none" and rule_id not in self.on_demand:
            self.log_info("[" + rule_id + "][" + rule["severity"] + "] " +
                          alert_text)
            if rule["severity"] != "debug":
                # ask db to save the alert
                message = Message(self)
                message.recipient = "controller/db"
                message.command = "SAVE_ALERT"
                message.args = rule["severity"]
                message.set_data(alert_text)
                self.send(message)
                # trigger output modules for notifications
                message = Message(self)
                message.recipient = "*/*"
                message.command = "NOTIFY"
                message.args = rule["severity"] + "/" + rule_id
                message.set_data(alert_text)
                self.send(message)
        # 5) if rule is manually requested to run, respond back
        if rule_id in self.on_demand:
            # retrieve original message
            message = self.on_demand[rule_id]
            message.reply()
            message.set_data(alert_text)
            self.send(message)
            del self.on_demand[rule_id]
        # 6) clean up, rule completed execution, remove the rule_id from the request queue and all the collected values
        del self.requests[rule_id][macro]
        if len(self.requests[rule_id]) == 0: del self.requests[rule_id]
        del self.values[rule_id][macro]
        if len(self.values[rule_id]) == 0: del self.values[rule_id]

    # add a rule. Continues in run_rule() when rule is executed
    def add_rule(self, rule_id, rule):
        self.log_debug("Received configuration for rule " + rule_id)
        # clean it up first
        self.remove_rule(rule_id)
        # if macros are defined, repeat the same independently for each macro
        macros = rule["macros"] if "macros" in rule else ["_default_"]
        for macro in macros:
            # create a copy of the rule and keep track of it
            rule_i = copy.deepcopy(rule)
            if rule_id not in self.rules: self.rules[rule_id] = {}
            self.rules[rule_id][macro] = rule_i
            # for each variable
            if "variables" in rule_i:
                for variable_id in rule_i["variables"]:
                    # replace the macro placeholder if any
                    rule_i["variables"][variable_id] = rule_i["variables"][
                        variable_id].replace("%i%", macro)
                    # process the variable content (0: request, 1: start, 2: end, 3: sensor_id)
                    match = re.match(self.variable_regexp,
                                     rule_i["variables"][variable_id])
                    if match is None: return
                    command, start, end, sensor_id = match.groups()
                    # request sensor's configuration for each variable, will be used when formatting the notification text
                    if command == "":
                        # remove any sub query (e.g. day/avg)
                        sensor_name = re.sub(r'\/(day|hour)\/[^\/]+$', '',
                                             sensor_id)
                        if sensor_name not in self.sensors:
                            # request the sensor's configuration
                            self.add_configuration_listener(
                                "sensors/" + sensor_name,
                                self.sensors_config_schema)
                            # keep track of the associated between this variable_id and the sensor
                            if rule_id not in self.variables:
                                self.variables[rule_id] = {}
                            if macro not in self.variables[rule_id]:
                                self.variables[rule_id][macro] = {}
                            self.variables[rule_id][macro][
                                variable_id] = sensor_name
            # for each action
            if "actions" in rule_i:
                # replace the macro placeholder if any
                for i in range(0, len(rule_i["actions"])):
                    rule_i["actions"][i] = rule_i["actions"][i].replace(
                        "%i%", macro)
            # for each trigger
            if "triggers" in rule_i:
                # replace the macro placeholder if any
                for i in range(0, len(rule_i["triggers"])):
                    rule_i["triggers"][i] = rule_i["triggers"][i].replace(
                        "%i%", macro)
            # if the rule is recurrent, we need to schedule its execution
            if rule_i["type"] == "recurrent":
                # schedule the rule execution
                self.log_debug("[" + rule_id + "][" + macro +
                               "] scheduling with the following settings: " +
                               str(rule_i["schedule"]))
                # "schedule" contains apscheduler settings for this rule
                job = rule_i["schedule"]
                # add function to call and args
                job["func"] = self.run_rule
                job["args"] = [rule_id, macro]
                # schedule the job for execution and keep track of the job id
                if rule_id not in self.jobs: self.jobs[rule_id] = {}
                self.jobs[rule_id][macro] = self.scheduler.add_job(job).id
            # if the rule is realtime, add each sensor_id to the triggers so the rule will be run upon any change
            if rule_i["type"] == "realtime":
                if "triggers" in rule_i:
                    for sensor_id in rule_i["triggers"]:
                        if sensor_id not in self.triggers:
                            self.triggers[sensor_id] = []
                        self.triggers[sensor_id].append(rule_id + "!" + macro)
                else:
                    self.log_warning(
                        "rule " + rule_id +
                        " is of type realtime but no triggers are defined")
        # retrieve the sensor for each macro
        if "macros" in rule:
            for sensor_id in rule["macros"]:
                if sensor_id not in self.sensors:
                    # request the sensor's configuration
                    self.add_configuration_listener("sensors/" + sensor_id,
                                                    self.sensors_config_schema)

    # remove a rule
    def remove_rule(self, rule_id):
        if rule_id in self.rules:
            self.log_debug("Removing rule " + rule_id)
            # delete the rule from every data structure
            del self.rules[rule_id]
            if rule_id in self.jobs:
                for macro in self.jobs[rule_id]:
                    self.scheduler.remove_job(self.jobs[rule_id][macro])
            if rule_id in self.requests: del self.requests[rule_id]
            if rule_id in self.values: del self.values[rule_id]
            triggers = copy.deepcopy(self.triggers)
            for sensor_id in triggers:
                rules = list(self.triggers[sensor_id])
                for rule in rules:
                    if rule.startswith(rule_id + "!"):
                        self.triggers[sensor_id].remove(rule)
                if len(self.triggers[sensor_id]) == 0:
                    del self.triggers[sensor_id]

    # apply configured retention policies for saved alerts
    def retention_policies(self):
        # ask the database module to purge the data
        message = Message(self)
        message.recipient = "controller/db"
        message.command = "PURGE_ALERTS"
        message.set_data(self.config["retention"])
        self.send(message)

    # What to do when running
    def on_start(self):
        # ask for all rules' configuration
        self.add_configuration_listener("rules/#", "+")
        # schedule to apply configured retention policies (every day just after 1am)
        job = {
            "func": self.retention_policies,
            "trigger": "cron",
            "hour": 1,
            "minute": 0,
            "second": sdk.python.utils.numbers.randint(1, 59)
        }
        self.scheduler.add_job(job)
        # start the scheduler
        self.scheduler.start()

    # What to do when shutting down
    def on_stop(self):
        self.scheduler.stop()

    # if notification suppression is configured, check if already hit the limit and if so return True, False otherwise
    def filter_notification(self, rule_id, rule):
        # if there is no filter configured, return
        if "suppress" not in rule:
            return False
        configuration = rule["suppress"]
        if "rate_hour" not in configuration:
            return False
        # get the current hour
        current_hour = int(time.strftime("%H"))
        # if this is the first time we run, initialize the data structure
        if rule_id not in self.notifications:
            self.notifications[rule_id] = {}
            self.notifications[rule_id]["hour"] = current_hour
            self.notifications[rule_id]["counter"] = 0
        # if this is a new hour, reset the notification counter
        if self.notifications[rule_id]["hour"] != current_hour:
            self.notifications[rule_id]["counter"] = 0
            self.notifications[rule_id]["hour"] = current_hour
        # check if rate limit is configured and we have not exceed the number of notifications during this hour
        if "rate_hour" in configuration and configuration[
                "rate_hour"] != 0 and self.notifications[rule_id][
                    "counter"] >= configuration["rate_hour"]:
            return True
        # increase the counter
        self.notifications[rule_id][
            "counter"] = self.notifications[rule_id]["counter"] + 1
        self.log_debug("notification #" +
                       str(self.notifications[rule_id]["counter"]) +
                       " for hour " +
                       str(self.notifications[rule_id]["hour"]) + ":00")

    # What to do when receiving a request for this module. Continues in evaluate_rule() once all the variables are set
    def on_message(self, message):
        # handle responses from the database
        if message.sender == "controller/db" and message.command.startswith(
                "GET"):
            session = self.sessions.restore(message)
            if session is None: return
            # cache the value of the variable
            self.log_debug("[" + session["rule_id"] + "][" + session["macro"] +
                           "] received from db " + session["variable_id"] +
                           ": " + str(message.get("data")))
            self.values[session["rule_id"]][session["macro"]][
                session["variable_id"]] = message.get("data")
            # remove the request_id from the queue of the rule
            self.requests[session["rule_id"]][session["macro"]].remove(
                message.get_request_id())
            # if there is only the LAST element in the queue, we have all the values, ready to evaluate the rule
            if len(self.requests[session["rule_id"]][
                    session["macro"]]) == 1 and self.requests[
                        session["rule_id"]][session["macro"]][0] == "LAST":
                self.evaluate_rule(session["rule_id"], session["macro"])
        # run a given rule
        elif message.command == "RUN":
            rule_id = message.args
            if message.sender == "controller/chatbot":
                self.on_demand[rule_id] = message
            self.run_rule(rule_id)
        # the database just stored a new value, print it out since we have the sensor's context
        elif message.sender == "controller/db" and message.command == "SAVED":
            sensor_id = message.args
            if message.has("group_by"):
                sensor_id = sensor_id + "/" + message.get("group_by")
            # check if a rule has this sensor_id among its variables, if so, run it
            for sensor_i in self.triggers:
                # sensor can contain also e.g. day/avg, check if starts with sensor_id
                if sensor_i.startswith(sensor_id):
                    for rule in self.triggers[sensor_i]:
                        rule_id, macro = rule.split("!")
                        self.run_rule(rule_id, macro)

    # What to do when receiving a rule
    def on_configuration(self, message):
        # ignore deleted configuration files while service is restarting
        if message.is_null and not message.args.startswith("rules/"): return
        # module's configuration
        if message.args == self.fullname and not message.is_null:
            # upgrade the config schema
            if message.config_schema == 1:
                config = message.get_data()
                config["loop_safeguard"] = 3
                self.upgrade_config(message.args, message.config_schema, 2,
                                    config)
                return False
            if message.config_schema != self.config_schema:
                return False
            # ensure the configuration file contains all required settings
            if not self.is_valid_configuration(["retention", "loop_safeguard"],
                                               message.get_data()):
                return False
            self.config = message.get_data()
        # add/remove sensors
        elif message.args.startswith("sensors/"):
            sensor_id = message.args.replace("sensors/", "")
            # deleted sensor
            if message.is_null:
                if sensor_id in self.sensors:
                    del self.sensors[sensor_id]
            # receiving sensor configuration
            else:
                self.sensors[sensor_id] = message.get_data()
        # add/remove rule
        elif message.args.startswith("rules/"):
            if not self.configured:
                return
            # upgrade the rule schema
            if message.config_schema == 1 and not message.is_null:
                rule = message.get_data()
                if "for" in rule:
                    rule["macros"] = rule["for"]
                    del rule["for"]
                self.upgrade_config(message.args, message.config_schema, 2,
                                    rule)
                return
            if message.config_schema != self.rules_config_schema:
                return
            rule_id = message.args.replace("rules/", "")
            if message.is_null:
                self.remove_rule(rule_id)
            else:
                rule = message.get_data()
                if not self.is_valid_configuration(
                    ["text", "type", "severity"], rule):
                    return
                if "disabled" in rule and rule["disabled"]:
                    self.remove_rule(rule_id)
                else:
                    self.add_rule(rule_id, rule)