forked from platypush/platypush
- 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()
|
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:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
from platypush.config import Config
|
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.message.request import Request
|
||||||
from platypush.utils import get_event_class_by_type
|
from platypush.utils import get_event_class_by_type
|
||||||
|
|
||||||
|
@ -73,11 +74,31 @@ class EventAction(Request):
|
||||||
|
|
||||||
|
|
||||||
def execute(self, **context):
|
def execute(self, **context):
|
||||||
for (key, value) in context.items():
|
event_args = context.pop('event').args if 'event' in context else {}
|
||||||
self.args[key] = value
|
|
||||||
|
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()
|
super().execute()
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def build(cls, action):
|
def build(cls, action):
|
||||||
action = super().parse(action)
|
action = super().parse(action)
|
||||||
|
@ -137,7 +158,7 @@ class EventHook(object):
|
||||||
logging.info('Running hook {} triggered by an event'.format(self.name))
|
logging.info('Running hook {} triggered by an event'.format(self.name))
|
||||||
|
|
||||||
for action in self.actions:
|
for action in self.actions:
|
||||||
action.execute(**result.parsed_args)
|
action.execute(event=event, **result.parsed_args)
|
||||||
|
|
||||||
|
|
||||||
# vim:sw=4:ts=4:et:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
|
|
||||||
from ..hook import EventHook
|
from ..hook import EventHook
|
||||||
|
|
||||||
|
@ -22,10 +23,10 @@ class EventProcessor(object):
|
||||||
|
|
||||||
|
|
||||||
def process_event(self, event):
|
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 = []
|
matched_hooks = []
|
||||||
max_score = 0
|
max_score = -sys.maxsize
|
||||||
|
|
||||||
for hook in self.hooks:
|
for hook in self.hooks:
|
||||||
match = hook.matches_event(event)
|
match = hook.matches_event(event)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import json
|
import json
|
||||||
import random
|
import random
|
||||||
|
import re
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
from platypush.config import Config
|
from platypush.config import Config
|
||||||
|
@ -34,36 +35,12 @@ class Event(Message):
|
||||||
event_type = msg['args'].pop('type')
|
event_type = msg['args'].pop('type')
|
||||||
event_class = get_event_class_by_type(event_type)
|
event_class = get_event_class_by_type(event_type)
|
||||||
|
|
||||||
args = {
|
args = msg['args'] if 'args' in msg else {}
|
||||||
'target' : msg['target'],
|
|
||||||
'origin' : msg['origin'],
|
|
||||||
**(msg['args'] if 'args' in msg else {}),
|
|
||||||
}
|
|
||||||
|
|
||||||
args['id'] = msg['id'] if 'id' in msg else cls._generate_id()
|
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)
|
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
|
@staticmethod
|
||||||
def _generate_id():
|
def _generate_id():
|
||||||
|
@ -73,6 +50,107 @@ class Event(Message):
|
||||||
id += '%.2x' % random.randint(0, 255)
|
id += '%.2x' % random.randint(0, 255)
|
||||||
return id
|
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):
|
def __str__(self):
|
||||||
"""
|
"""
|
||||||
Overrides the str() operator and converts
|
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
|
the match is - in case of multiple event matches, the ones with the
|
||||||
highest score will win """
|
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.is_match = is_match
|
||||||
self.score = score
|
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
|
# XXX Should be a stop Request, not an Event
|
||||||
|
|
|
@ -27,35 +27,9 @@ class SpeechRecognizedEvent(AssistantEvent):
|
||||||
self.recognized_phrase = phrase.strip().lower()
|
self.recognized_phrase = phrase.strip().lower()
|
||||||
|
|
||||||
def matches_condition(self, condition):
|
def matches_condition(self, condition):
|
||||||
result = EventMatchResult(is_match=False, score=0)
|
result = super().matches_condition(condition)
|
||||||
|
if result.is_match and self._assistant:
|
||||||
if not isinstance(self, condition.type): return result
|
self._assistant.stop_conversation()
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ from platypush.utils import get_module_and_method_from_action
|
||||||
class Request(Message):
|
class Request(Message):
|
||||||
""" Request message class """
|
""" 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:
|
Params:
|
||||||
target -- Target node [String]
|
target -- Target node [String]
|
||||||
|
@ -28,7 +28,7 @@ class Request(Message):
|
||||||
self.target = target
|
self.target = target
|
||||||
self.action = action
|
self.action = action
|
||||||
self.origin = origin
|
self.origin = origin
|
||||||
self.args = args
|
self.args = args if args else {}
|
||||||
self.backend = backend
|
self.backend = backend
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -65,11 +65,21 @@ class Pusher(object):
|
||||||
"~/.config/platypush/config.yaml or " +
|
"~/.config/platypush/config.yaml or " +
|
||||||
"/etc/platypush/config.yaml")
|
"/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")
|
help="Destination of the command")
|
||||||
|
|
||||||
parser.add_argument('--action', '-a', dest='action', required=True,
|
parser.add_argument('--action', '-a', dest='action', required=False,
|
||||||
help="Action to execute, as package.method")
|
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,
|
parser.add_argument('--backend', '-b', dest='backend', required=False,
|
||||||
default=None, help="Backend to deliver the message " +
|
default=None, help="Backend to deliver the message " +
|
||||||
|
@ -86,8 +96,17 @@ class Pusher(object):
|
||||||
opts, args = parser.parse_known_args(args)
|
opts, args = parser.parse_known_args(args)
|
||||||
|
|
||||||
if len(args) % 2 != 0:
|
if len(args) % 2 != 0:
|
||||||
|
parser.print_help()
|
||||||
raise RuntimeError('Odd number of key-value options passed: {}'.format(args))
|
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 = {}
|
opts.args = {}
|
||||||
for i in range(0, len(args), 2):
|
for i in range(0, len(args), 2):
|
||||||
opts.args[re.sub('^-+', '', args[i])] = args[i+1]
|
opts.args[re.sub('^-+', '', args[i])] = args[i+1]
|
||||||
|
@ -109,7 +128,21 @@ class Pusher(object):
|
||||||
# self.backend_instance.stop()
|
# self.backend_instance.stop()
|
||||||
return _f
|
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):
|
timeout=default_response_wait_timeout, **kwargs):
|
||||||
"""
|
"""
|
||||||
Sends a message on a backend and optionally waits for an answer.
|
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
|
timeout -- Response receive timeout in seconds
|
||||||
- Pusher Default: 5 seconds
|
- Pusher Default: 5 seconds
|
||||||
- If timeout == 0 or None: Pusher exits without waiting for a response
|
- 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
|
**kwargs -- Optional key-valued arguments for the action method
|
||||||
(e.g. cmd='echo ping' or groups="['Living Room']")
|
(e.g. cmd='echo ping' or groups="['Living Room']")
|
||||||
"""
|
"""
|
||||||
|
@ -146,18 +176,5 @@ class Pusher(object):
|
||||||
response_timeout=timeout)
|
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:
|
# vim:sw=4:ts=4:et:
|
||||||
|
|
||||||
|
|
4
setup.py
4
setup.py
|
@ -43,8 +43,8 @@ setup(
|
||||||
packages = find_packages(),
|
packages = find_packages(),
|
||||||
entry_points = {
|
entry_points = {
|
||||||
'console_scripts': [
|
'console_scripts': [
|
||||||
'platypush=platypush:main',
|
'platypush=platypush.__main__:main',
|
||||||
'pusher=platypush.pusher:main',
|
'pusher=platypush.pusher.__main__:main',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
data_files = [
|
data_files = [
|
||||||
|
|
Loading…
Reference in a new issue