platypush/platypush/message/event/__init__.py

239 lines
8.4 KiB
Python
Raw Normal View History

2018-01-10 03:14:27 +01:00
import copy
import json
import random
import re
import sys
import time
2018-01-10 03:14:27 +01:00
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]
"""
2018-10-08 15:30:00 +02:00
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():
2021-11-14 19:43:19 +01:00
if arg not in [
'id',
'args',
'origin',
'target',
'type',
'timestamp',
'disable_logging',
2021-11-14 19:43:19 +01:00
] and not arg.startswith('_'):
2020-05-09 01:47:12 +02:00
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):
"""
2017-12-24 13:15:37 +01:00
If the event matches an event condition, it will return an EventMatchResult
2020-01-09 23:40:59 +01:00
: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:
2017-12-24 13:15:37 +01:00
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
2017-12-24 13:15:37 +01:00
return result
2017-12-24 13:15:37 +01:00
result.is_match = True
if match_scores:
result.score = sum(match_scores) / float(len(match_scores))
2017-12-24 13:15:37 +01:00
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:
2021-02-24 01:34:41 +01:00
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
"""
2018-01-10 03:14:27 +01:00
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"""
2017-12-24 13:15:37 +01:00
def __init__(self, is_match, score=0, parsed_args=None):
2017-12-24 13:15:37 +01:00
self.is_match = is_match
self.score = score
self.parsed_args = parsed_args or {}
2017-12-24 13:15:37 +01:00
2018-01-10 03:14:27 +01:00
def flatten(args):
if isinstance(args, dict):
for (key, value) in args.items():
2018-01-10 03:14:27 +01:00
if isinstance(value, date):
args[key] = value.isoformat()
elif isinstance(value, (dict, list)):
2018-01-10 03:14:27 +01:00
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)):
2018-01-10 03:14:27 +01:00
flatten(args[i])
# vim:sw=4:ts=4:et: