- Proper support for event arguments
- Better algorithm for event matching - Added send_event support to pusher
This commit is contained in:
parent
08189653bb
commit
5a92c0ac3b
8 changed files with 179 additions and 96 deletions
|
@ -127,13 +127,5 @@ class Daemon(object):
|
|||
self.stop_app()
|
||||
|
||||
|
||||
def main(args=sys.argv[1:]):
|
||||
print('Starting platypush v.{}'.format(__version__))
|
||||
app = Daemon.build_from_cmdline(args)
|
||||
app.start()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
||||
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import json
|
||||
import logging
|
||||
import re
|
||||
|
||||
from platypush.config import Config
|
||||
from platypush.message.event import Event
|
||||
from platypush.message.event import Event, EventMatchResult
|
||||
from platypush.message.request import Request
|
||||
from platypush.utils import get_event_class_by_type
|
||||
|
||||
|
@ -73,11 +74,31 @@ class EventAction(Request):
|
|||
|
||||
|
||||
def execute(self, **context):
|
||||
for (key, value) in context.items():
|
||||
self.args[key] = value
|
||||
event_args = context.pop('event').args if 'event' in context else {}
|
||||
|
||||
for (argname, value) in self.args.items():
|
||||
if isinstance(value, str):
|
||||
parsed_value = ''
|
||||
while value:
|
||||
m = re.match('([^\\\]*)\$([\w\d_-]+)(.*)', value)
|
||||
if m:
|
||||
context_argname = m.group(2)
|
||||
value = m.group(3)
|
||||
if context_argname in context:
|
||||
parsed_value += m.group(1) + context[context_argname]
|
||||
else:
|
||||
parsed_value += m.group(1) + '$' + m.group(2)
|
||||
else:
|
||||
parsed_value += value
|
||||
value = ''
|
||||
|
||||
value = parsed_value
|
||||
|
||||
self.args[argname] = value
|
||||
|
||||
super().execute()
|
||||
|
||||
|
||||
@classmethod
|
||||
def build(cls, action):
|
||||
action = super().parse(action)
|
||||
|
@ -137,7 +158,7 @@ class EventHook(object):
|
|||
logging.info('Running hook {} triggered by an event'.format(self.name))
|
||||
|
||||
for action in self.actions:
|
||||
action.execute(**result.parsed_args)
|
||||
action.execute(event=event, **result.parsed_args)
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import logging
|
||||
import sys
|
||||
|
||||
from ..hook import EventHook
|
||||
|
||||
|
@ -22,10 +23,10 @@ class EventProcessor(object):
|
|||
|
||||
|
||||
def process_event(self, event):
|
||||
""" Processes an event and runs any matched hooks """
|
||||
""" Processes an event and runs the matched hooks with the highest score """
|
||||
|
||||
matched_hooks = []
|
||||
max_score = 0
|
||||
max_score = -sys.maxsize
|
||||
|
||||
for hook in self.hooks:
|
||||
match = hook.matches_event(event)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import json
|
||||
import random
|
||||
import re
|
||||
import threading
|
||||
|
||||
from platypush.config import Config
|
||||
|
@ -34,36 +35,12 @@ class Event(Message):
|
|||
event_type = msg['args'].pop('type')
|
||||
event_class = get_event_class_by_type(event_type)
|
||||
|
||||
args = {
|
||||
'target' : msg['target'],
|
||||
'origin' : msg['origin'],
|
||||
**(msg['args'] if 'args' in msg else {}),
|
||||
}
|
||||
|
||||
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')
|
||||
return event_class(**args)
|
||||
|
||||
def matches_condition(self, condition):
|
||||
"""
|
||||
If the event matches an event condition, it will return an EventMatchResult
|
||||
Params:
|
||||
-- condition -- The platypush.event.hook.EventCondition object
|
||||
"""
|
||||
|
||||
result = EventMatchResult(is_match=False)
|
||||
if not isinstance(self, condition.type): return result
|
||||
|
||||
for (attr, value) in condition.args.items():
|
||||
if not hasattr(self.args, attr):
|
||||
return result
|
||||
if isinstance(self.args[attr], str) and not value in self.args[attr]:
|
||||
return result
|
||||
elif self.args[attr] != value:
|
||||
return result
|
||||
|
||||
result.is_match = True
|
||||
return result
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _generate_id():
|
||||
|
@ -73,6 +50,107 @@ class Event(Message):
|
|||
id += '%.2x' % random.randint(0, 255)
|
||||
return id
|
||||
|
||||
|
||||
def matches_condition(self, condition):
|
||||
"""
|
||||
If the event matches an event condition, it will return an EventMatchResult
|
||||
Params:
|
||||
-- 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)
|
||||
event_tokens = re.split('\s+', self.args[argname].strip().lower())
|
||||
condition_tokens = re.split('\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
|
||||
elif re.search(condition_token, event_token):
|
||||
# The only supported regex-match as of now is the equivalent of
|
||||
# the maybe operator.
|
||||
# e.g. "turn on (the)? lights" would match both "turn on the lights"
|
||||
# and "turn on lights". In such a case, we just consume the
|
||||
# condition token and proceed forward. TODO add a more
|
||||
# sophisticated regex-match handling
|
||||
condition_tokens.pop(0)
|
||||
else:
|
||||
m = re.match('[^\\\]*\$([\w\d_-]+)', condition_token)
|
||||
if m:
|
||||
argname = m.group(1)
|
||||
if argname not in result.parsed_args:
|
||||
result.parsed_args[argname] = event_token
|
||||
result.score += 1
|
||||
else:
|
||||
result.parsed_args[argname] += ' ' + event_token
|
||||
|
||||
|
||||
if 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
|
||||
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
|
||||
|
@ -98,10 +176,10 @@ class EventMatchResult(object):
|
|||
the match is - in case of multiple event matches, the ones with the
|
||||
highest score will win """
|
||||
|
||||
def __init__(self, is_match, score=1, parsed_args = {}):
|
||||
def __init__(self, is_match, score=0, parsed_args=None):
|
||||
self.is_match = is_match
|
||||
self.score = score
|
||||
self.parsed_args = parsed_args
|
||||
self.parsed_args = {} if not parsed_args else parsed_args
|
||||
|
||||
|
||||
# XXX Should be a stop Request, not an Event
|
||||
|
|
|
@ -27,35 +27,9 @@ class SpeechRecognizedEvent(AssistantEvent):
|
|||
self.recognized_phrase = phrase.strip().lower()
|
||||
|
||||
def matches_condition(self, condition):
|
||||
result = EventMatchResult(is_match=False, score=0)
|
||||
|
||||
if not isinstance(self, condition.type): return result
|
||||
|
||||
recognized_tokens = re.split('\s+', self.recognized_phrase.strip().lower())
|
||||
condition_tokens = re.split('\s+', condition.args['phrase'].strip().lower())
|
||||
|
||||
while recognized_tokens and condition_tokens:
|
||||
rec_token = recognized_tokens[0]
|
||||
cond_token = condition_tokens[0]
|
||||
|
||||
if rec_token == cond_token:
|
||||
recognized_tokens.pop(0)
|
||||
condition_tokens.pop(0)
|
||||
result.score += 1
|
||||
elif re.search(cond_token, rec_token):
|
||||
condition_tokens.pop(0)
|
||||
else:
|
||||
m = re.match('^\$([\w\d_])', cond_token)
|
||||
if m:
|
||||
result.parsed_args[cond_token[1:]] = rec_token
|
||||
recognized_tokens.pop(0)
|
||||
condition_tokens.pop(0)
|
||||
result.score += 1
|
||||
else:
|
||||
recognized_tokens.pop(0)
|
||||
|
||||
result.is_match = len(condition_tokens) == 0
|
||||
if result.is_match and self._assistant: self._assistant.stop_conversation()
|
||||
result = super().matches_condition(condition)
|
||||
if result.is_match and self._assistant:
|
||||
self._assistant.stop_conversation()
|
||||
|
||||
return result
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ from platypush.utils import get_module_and_method_from_action
|
|||
class Request(Message):
|
||||
""" Request message class """
|
||||
|
||||
def __init__(self, target, action, origin=None, id=None, backend=None, args={}):
|
||||
def __init__(self, target, action, origin=None, id=None, backend=None, args=None):
|
||||
"""
|
||||
Params:
|
||||
target -- Target node [String]
|
||||
|
@ -28,7 +28,7 @@ class Request(Message):
|
|||
self.target = target
|
||||
self.action = action
|
||||
self.origin = origin
|
||||
self.args = args
|
||||
self.args = args if args else {}
|
||||
self.backend = backend
|
||||
|
||||
@classmethod
|
||||
|
|
|
@ -65,11 +65,21 @@ class Pusher(object):
|
|||
"~/.config/platypush/config.yaml or " +
|
||||
"/etc/platypush/config.yaml")
|
||||
|
||||
parser.add_argument('--target', '-t', dest='target', required=True,
|
||||
parser.add_argument('--type', '-p', dest='type', required=False,
|
||||
default='request', help="Type of message to send, request or event")
|
||||
|
||||
parser.add_argument('--target', '-t', dest='target', required=False,
|
||||
default=Config.get('device_id'),
|
||||
help="Destination of the command")
|
||||
|
||||
parser.add_argument('--action', '-a', dest='action', required=True,
|
||||
help="Action to execute, as package.method")
|
||||
parser.add_argument('--action', '-a', dest='action', required=False,
|
||||
default=None, help="Action to execute, as " +
|
||||
"package.method (e.g. music.mpd.play), if this is a request")
|
||||
|
||||
parser.add_argument('--event', '-e', dest='event', required=False,
|
||||
default=None, help="Event type, as full " +
|
||||
"package.class (e.g. " +
|
||||
"platypush.message.event.ping.PingEvent), if this is an event")
|
||||
|
||||
parser.add_argument('--backend', '-b', dest='backend', required=False,
|
||||
default=None, help="Backend to deliver the message " +
|
||||
|
@ -86,8 +96,17 @@ class Pusher(object):
|
|||
opts, args = parser.parse_known_args(args)
|
||||
|
||||
if len(args) % 2 != 0:
|
||||
parser.print_help()
|
||||
raise RuntimeError('Odd number of key-value options passed: {}'.format(args))
|
||||
|
||||
if opts.type == 'request' and not opts.action:
|
||||
parser.print_help()
|
||||
raise RuntimeError('No action provided for the request'.format(args))
|
||||
|
||||
if opts.type == 'event' and not opts.event:
|
||||
parser.print_help()
|
||||
raise RuntimeError('No type provided for the event'.format(args))
|
||||
|
||||
opts.args = {}
|
||||
for i in range(0, len(args), 2):
|
||||
opts.args[re.sub('^-+', '', args[i])] = args[i+1]
|
||||
|
@ -109,7 +128,21 @@ class Pusher(object):
|
|||
# self.backend_instance.stop()
|
||||
return _f
|
||||
|
||||
def push(self, target, action, backend=None, config_file=None,
|
||||
def send_event(self, target=Config.get('device_id'),
|
||||
type='platypush.message.event.Event', backend=None, **kwargs):
|
||||
if not backend: backend = self.backend
|
||||
|
||||
self.backend_instance = self.get_backend(backend)
|
||||
self.backend_instance.send_event({
|
||||
'target': target,
|
||||
'args': {
|
||||
'type': type,
|
||||
**kwargs
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
def push(self, target, action, backend=None,
|
||||
timeout=default_response_wait_timeout, **kwargs):
|
||||
"""
|
||||
Sends a message on a backend and optionally waits for an answer.
|
||||
|
@ -123,9 +156,6 @@ class Pusher(object):
|
|||
timeout -- Response receive timeout in seconds
|
||||
- Pusher Default: 5 seconds
|
||||
- If timeout == 0 or None: Pusher exits without waiting for a response
|
||||
config_file -- Path to the configuration file to be used (default:
|
||||
~/.config/platypush/config.yaml or
|
||||
/etc/platypush/config.yaml)
|
||||
**kwargs -- Optional key-valued arguments for the action method
|
||||
(e.g. cmd='echo ping' or groups="['Living Room']")
|
||||
"""
|
||||
|
@ -146,18 +176,5 @@ class Pusher(object):
|
|||
response_timeout=timeout)
|
||||
|
||||
|
||||
def main(args=sys.argv[1:]):
|
||||
opts = Pusher.parse_build_args(args)
|
||||
|
||||
pusher = Pusher(config_file=opts.config, backend=opts.backend)
|
||||
|
||||
pusher.push(opts.target, action=opts.action, timeout=opts.timeout,
|
||||
**opts.args)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
||||
|
||||
|
|
4
setup.py
4
setup.py
|
@ -43,8 +43,8 @@ setup(
|
|||
packages = find_packages(),
|
||||
entry_points = {
|
||||
'console_scripts': [
|
||||
'platypush=platypush:main',
|
||||
'pusher=platypush.pusher:main',
|
||||
'platypush=platypush.__main__:main',
|
||||
'pusher=platypush.pusher.__main__:main',
|
||||
],
|
||||
},
|
||||
data_files = [
|
||||
|
|
Loading…
Reference in a new issue