import copy import json import random import re import sys import time from datetime import date from platypush.config import Config from platypush.message import Message from platypush.utils import get_event_class_by_type class Event(Message): """Event message class""" # If this class property is set to false then the logging of these events # will be disabled. Logging is usually disabled for events with a very # high frequency that would otherwise pollute the logs e.g. camera capture # events # pylint: disable=redefined-builtin def __init__( self, target=None, origin=None, id=None, timestamp=None, disable_logging=False, disable_web_clients_notification=False, **kwargs ): """ Params: target -- Target node [String] origin -- Origin node (default: current node) [String] id -- Event ID (default: auto-generated) kwargs -- Additional arguments for the event [kwDict] """ super().__init__(timestamp=timestamp) self.id = id if id else self._generate_id() self.target = target if target else Config.get('device_id') self.origin = origin if origin else Config.get('device_id') self.type = '{}.{}'.format(self.__class__.__module__, self.__class__.__name__) self.args = kwargs self.disable_logging = disable_logging self.disable_web_clients_notification = disable_web_clients_notification for arg, value in self.args.items(): if arg not in [ 'id', 'args', 'origin', 'target', 'type', 'timestamp', 'disable_logging', ] and not arg.startswith('_'): self.__setattr__(arg, value) @classmethod def build(cls, msg): """Builds an event message from a JSON UTF-8 string/bytearray, a dictionary, or another Event""" msg = super().parse(msg) event_type = msg['args'].pop('type') event_class = get_event_class_by_type(event_type) args = msg['args'] if 'args' in msg else {} args['id'] = msg['id'] if 'id' in msg else cls._generate_id() args['target'] = msg['target'] if 'target' in msg else Config.get('device_id') args['origin'] = msg['origin'] if 'origin' in msg else Config.get('device_id') args['timestamp'] = msg['_timestamp'] if '_timestamp' in msg else time.time() return event_class(**args) @staticmethod def _generate_id(): """Generate a unique event ID""" return ''.join(['{:02x}'.format(random.randint(0, 255)) for _ in range(16)]) def matches_condition(self, condition): """ If the event matches an event condition, it will return an EventMatchResult :param condition: The platypush.event.hook.EventCondition object """ result = EventMatchResult(is_match=False, parsed_args=self.args) match_scores = [] if not isinstance(self, condition.type): return result for (attr, value) in condition.args.items(): if attr not in self.args: return result if isinstance(self.args[attr], str): arg_result = self._matches_argument(argname=attr, condition_value=value) if arg_result.is_match: match_scores.append(arg_result.score) for (parsed_arg, parsed_value) in arg_result.parsed_args.items(): result.parsed_args[parsed_arg] = parsed_value else: return result elif self.args[attr] != value: # TODO proper support for list and dictionary matches return result result.is_match = True if match_scores: result.score = sum(match_scores) / float(len(match_scores)) return result def _matches_argument(self, argname, condition_value): """ Returns an EventMatchResult if the event argument [argname] matches [condition_value]. - Example: - self.args = { 'phrase': 'Hey dude turn on the living room lights' } - self._matches_argument(argname='phrase', condition_value='Turn on the ${lights} lights') will return EventMatchResult(is_match=True, parsed_args={ 'lights': 'living room' }) - self._matches_argument(argname='phrase', condition_value='Turn off the ${lights} lights') will return EventMatchResult(is_match=False, parsed_args={}) """ result = EventMatchResult(is_match=False) if self.args.get(argname) == condition_value: # In case of an exact match, return immediately result.is_match = True result.score = sys.maxsize return result event_tokens = re.split(r'\s+', self.args.get(argname, '').strip().lower()) condition_tokens = re.split(r'\s+', condition_value.strip().lower()) while event_tokens and condition_tokens: event_token = event_tokens[0] condition_token = condition_tokens[0] if event_token == condition_token: event_tokens.pop(0) condition_tokens.pop(0) result.score += 1.5 elif re.search(condition_token, event_token): m = re.search('({})'.format(condition_token), event_token) if m.group(1): event_tokens.pop(0) result.score += 1.25 condition_tokens.pop(0) else: m = re.match(r'[^\\]*\${(.+?)}', condition_token) if m: argname = m.group(1) if argname not in result.parsed_args: result.parsed_args[argname] = event_token result.score += 1.0 else: result.parsed_args[argname] += ' ' + event_token if (len(condition_tokens) == 1 and len(event_tokens) == 1) or ( len(event_tokens) > 1 and len(condition_tokens) > 1 and event_tokens[1] == condition_tokens[1] ): # Stop appending tokens to this argument, as the next # condition will be satisfied as well condition_tokens.pop(0) event_tokens.pop(0) else: result.score -= 1.0 event_tokens.pop(0) # It's a match if all the tokens in the condition string have been satisfied result.is_match = len(condition_tokens) == 0 return result def __str__(self): """ Overrides the str() operator and converts the message into a UTF-8 JSON string """ args = copy.deepcopy(self.args) flatten(args) return json.dumps( { 'type': 'event', 'target': self.target, 'origin': self.origin if hasattr(self, 'origin') else None, 'id': self.id if hasattr(self, 'id') else None, '_timestamp': self.timestamp, 'args': {'type': self.type, **args}, }, cls=self.Encoder, ) class EventMatchResult: """When comparing an event against an event condition, you want to return this object. It contains the match status (True or False), any parsed arguments, and a match_score that identifies how "strong" the match is - in case of multiple event matches, the ones with the highest score will win""" def __init__(self, is_match, score=0, parsed_args=None): self.is_match = is_match self.score = score self.parsed_args = parsed_args or {} def flatten(args): if isinstance(args, dict): for (key, value) in args.items(): if isinstance(value, date): args[key] = value.isoformat() elif isinstance(value, (dict, list)): flatten(args[key]) elif isinstance(args, list): for i, arg in enumerate(args): if isinstance(arg, date): args[i] = arg.isoformat() elif isinstance(arg, (dict, list)): flatten(args[i]) # vim:sw=4:ts=4:et: