Pārlūkot izejas kodu

Merge pull request #313 from kalliope-project/neurotimer

add neurotimer core neuron
Monf 7 gadi atpakaļ
vecāks
revīzija
9c309121db

+ 2 - 3
kalliope/core/Models/MatchedSynapse.py

@@ -28,9 +28,8 @@ class MatchedSynapse(object):
             self.parameters = NeuronParameterLoader.get_parameters(synapse_order=self.matched_order,
                                                                    user_order=user_order)
         if overriding_parameter is not None:
-            # we suppose that we don't have any parameters.
-            # We replace the current parameter object with the received one
-            self.parameters = overriding_parameter
+            # merge dict of parameters with overriding
+            self.parameters.update(overriding_parameter)
 
         # list of Neuron Module
         self.neuron_module_list = list()

+ 16 - 0
kalliope/core/NeuronModule.py

@@ -6,6 +6,7 @@ import six
 
 from jinja2 import Template
 
+from kalliope.core.SynapseLauncher import SynapseLauncher
 from kalliope.core import OrderListener
 from kalliope.core.ConfigurationManager import SettingLoader, BrainLoader
 from kalliope.core.Models.MatchedSynapse import MatchedSynapse
@@ -315,3 +316,18 @@ class NeuronModule(object):
                     RpiUtils.switch_pin_to_on(rpi_settings.pin_led_talking)
                 else:
                     RpiUtils.switch_pin_to_off(rpi_settings.pin_led_talking)
+
+    def start_synapse_by_name(self, synapse_name, overriding_parameter_dict=None):
+        """
+        Used to run a synapse by name by calling directly the SynapseLauncher class.
+        The Lifo buffer is not aware of this call and so the user cannot get the result
+        :param synapse_name: name of the synapse to run
+        :param overriding_parameter_dict: dict of parameter to pass to the synapse
+        """
+        # received parameters are not coded with utf-8 on python 2 by default.
+        if sys.version_info[0] == 2:
+            reload(sys)
+            sys.setdefaultencoding('utf-8')
+        SynapseLauncher.start_synapse_by_name(synapse_name,
+                                              brain=self.brain,
+                                              overriding_parameter_dict=overriding_parameter_dict)

+ 4 - 2
kalliope/core/SynapseLauncher.py

@@ -23,11 +23,12 @@ class SynapseNameNotFound(Exception):
 class SynapseLauncher(object):
 
     @classmethod
-    def start_synapse_by_name(cls, name, brain=None):
+    def start_synapse_by_name(cls, name, brain=None, overriding_parameter_dict=None):
         """
         Start a synapse by it's name
         :param name: Name (Unique ID) of the synapse to launch
         :param brain: Brain instance
+        :param overriding_parameter_dict: parameter to pass to neurons
         """
         logger.debug("[SynapseLauncher] start_synapse_by_name called with synapse name: %s " % name)
         # check if we have found and launched the synapse
@@ -41,7 +42,8 @@ class SynapseLauncher(object):
             list_synapse_to_process = list()
             new_matching_synapse = MatchedSynapse(matched_synapse=synapse,
                                                   matched_order=None,
-                                                  user_order=None)
+                                                  user_order=None,
+                                                  overriding_parameter=overriding_parameter_dict)
             list_synapse_to_process.append(new_matching_synapse)
             lifo_buffer.add_synapse_list_to_lifo(list_synapse_to_process)
             return lifo_buffer.execute(is_api_call=True)

+ 120 - 0
kalliope/neurons/neurotimer/README.md

@@ -0,0 +1,120 @@
+# Neurotimer
+
+## Synopsis
+
+Run a synapse after a delay.
+
+## Installation
+
+CORE NEURON : No installation needed. 
+
+## Options
+
+| parameter            | required | type   | default | choices   | comment                                                      |
+|----------------------|----------|--------|---------|-----------|--------------------------------------------------------------|
+| seconds              | NO       | int    |         | value > 0 | Number of second to wait before running the synapse          |
+| minutes              | NO       | int    |         | value > 0 | Number of minutes to wait before running the synapse         |
+| hours                | NO       | int    |         | value > 0 | Number of hours to wait before running the synapse           |
+| synapse              | YES      | string |         |           | Name of the synapse to run after the selected delay          |
+| forwarded_parameters | NO       | dict   |         |           | dict of parameters that will be passed to the called synapse |
+
+## Return Values
+
+None
+
+## Synapses example
+
+
+**Scenario:** You are used to make a tea and want to know when it's time to remove the bag.
+> **You:** remember me to remove the bag of my tea
+**Kalliope:** Alright
+3 minutes later..
+**Kalliope:** your tea is ready
+
+```yml
+- name: "tea-bag"
+  signals:
+    - order: "remember me to remove the bag of my tea"
+  neurons:
+    - neurotimer:
+        minutes: 3
+        synapse: "time-over"
+    - say:
+        message:
+          - "Alright"
+
+- name: "time-over"
+  signals:
+     - order: "no-order-for-this-synapse"
+  neurons:
+    - say:
+        message:
+          - "your tea is ready" 
+```
+
+If your STT engine return integer when capturing a spoken order, you can set the time on the fly.
+**Scenario:** You are starting to cook something
+> **You:** notify me in 10 minutes
+**Kalliope:** I'll notify you in 10 minutes
+10 minutes later..
+**Kalliope:** You asked me to notify you
+
+```yml
+- name: "timer2"
+    signals:
+      - order: "notify me in {{ time }} minutes"
+    neurons:
+      - neurotimer:
+          minutes: "{{ time }}"
+          synapse: "notify"
+      - say:
+          message:
+            - "I'll notify you in {{ time }} minutes"
+
+- name: "notify"
+  signals:
+     - order: "no-order-for-this-synapse"
+  neurons:
+    - say:
+        message:
+          - "You asked me to notify you" 
+```
+
+Passing argument to the called synapse
+**Scenario:** You want to remember to do something
+> **You:** remind me to call mom in 15 minutes
+**Kalliope:** I'll notify you in 15 minutes
+15 minutes later..
+**Kalliope:** You asked me to remind you to call mom 15 minutes ago
+```yml
+- name: "remember-synapse"
+  signals:
+    - order: "remind me to {{ remember }} in {{ time }} minutes"
+  neurons:
+    - neurotimer:
+        seconds: "{{ time }}"
+        synapse: "remember-todo"
+        forwarded_parameters:
+          remember: "{{ remember }}"
+          seconds: "{{ time }}"
+    - say:
+        message:
+          - "I'll remind you in {{ time }} minutes"
+
+- name: "remember-todo"
+  signals:
+    - order: "no-order-for-this-synapse"
+  neurons:
+    - say:
+        message:
+          - "You asked me to remind you to {{ remember }} {{ time }} minutes ago"
+```
+
+
+## Notes
+
+> **Note:** When used from the API, returned value from the launched synapse are lost
+
+> **Note:** Not all STT engine return integer.
+
+> **Note:** You must set at least one timer parameter (seconds or minutes or hours). You can also set them all.

+ 0 - 0
kalliope/neurons/neurotimer/__init__.py


+ 121 - 0
kalliope/neurons/neurotimer/neurotimer.py

@@ -0,0 +1,121 @@
+import logging
+import threading
+
+import time
+
+from kalliope.core.NeuronModule import MissingParameterException, InvalidParameterException
+
+from kalliope.core import NeuronModule
+
+logging.basicConfig()
+logger = logging.getLogger("kalliope")
+
+
+class TimerThread(threading.Thread):
+    def __init__(self, time_to_wait_seconds, callback):
+        """
+        A Thread that will call the given callback method after waiting time_to_wait_seconds
+        :param time_to_wait_seconds: number of second to wait before call the callback method
+        :param callback: callback method
+        """
+        threading.Thread.__init__(self)
+        self.time_to_wait_seconds = time_to_wait_seconds
+        self.callback = callback
+
+    def run(self):
+        # wait the amount of seconds
+        logger.debug("[Neurotimer] wait %s seconds" % self.time_to_wait_seconds)
+        time.sleep(self.time_to_wait_seconds)
+        # then run the callback method
+        self.callback()
+
+
+class Neurotimer(NeuronModule):
+    def __init__(self, **kwargs):
+        super(Neurotimer, self).__init__(**kwargs)
+
+        # get parameters
+        self.seconds = kwargs.get('seconds', None)
+        self.minutes = kwargs.get('minutes', None)
+        self.hours = kwargs.get('hours', None)
+        self.synapse = kwargs.get('synapse', None)
+        self.forwarded_parameter = kwargs.get('forwarded_parameters', None)
+
+        # do some check
+        if self._is_parameters_ok():
+            # make the sum of all time parameter in seconds
+            retarding_time_seconds = self._get_retarding_time_seconds()
+
+            # now wait before running the target synapse
+            ds = TimerThread(time_to_wait_seconds=retarding_time_seconds, callback=self.callback_run_synapse)
+            # ds.daemon = True
+            ds.start()
+
+    def _is_parameters_ok(self):
+        """
+        Check given neuron parameters are valid
+        :return: True if the neuron has been well configured
+        """
+
+        # at least one time parameter must be set
+        if self.seconds is None and self.minutes is None and self.hours is None:
+            raise MissingParameterException("Neurotimer must have at least one time "
+                                            "parameter: seconds, minutes, hours")
+
+        self.seconds = self.get_integer_time_parameter(self.seconds)
+        self.minutes = self.get_integer_time_parameter(self.minutes)
+        self.hours = self.get_integer_time_parameter(self.hours)
+        if self.synapse is None:
+            raise MissingParameterException("Neurotimer must have a synapse name parameter")
+
+        return True
+
+    @staticmethod
+    def get_integer_time_parameter(time_parameter):
+        """
+        Check if a given time parameter is a valid integer:
+        - must be > 0
+        - if type no an integer, must be convertible to integer
+
+        :param time_parameter: string or integer
+        :return: integer
+        """
+        if time_parameter is not None:
+            if not isinstance(time_parameter, int):
+                # try to convert into integer
+                try:
+                    time_parameter = int(time_parameter)
+                except ValueError:
+                    raise InvalidParameterException("[Neurotimer] %s is not a valid integer" % time_parameter)
+            # check if positive
+            if time_parameter < 0:
+                raise InvalidParameterException("[Neurotimer] %s must be > 0" % time_parameter)
+
+        return time_parameter
+
+    def _get_retarding_time_seconds(self):
+        """
+        Return the sum of given time parameters
+        seconds + minutes + hours
+        :return: integer, number of total seconds
+        """
+        returned_time = 0
+
+        if self.seconds is not None:
+            returned_time += self.seconds
+        if self.minutes is not None:
+            returned_time += self.minutes * 60
+        if self.hours is not None:
+            returned_time += self.hours * 3600
+
+        logger.debug("[Neurotimer] get_retarding_time_seconds: %s" % returned_time)
+        return returned_time
+
+    def callback_run_synapse(self):
+        """
+        Callback method which will be started by the timer thread once the time is over
+        :return:
+        """
+        logger.debug("[Neurotimer] waiting time is over, start the synapse %s" % self.synapse)
+
+        self.start_synapse_by_name(synapse_name=self.synapse, overriding_parameter_dict=self.forwarded_parameter)

+ 1 - 1
kalliope/neurons/shell/README.md

@@ -100,7 +100,7 @@ If you want to add argument to your shell command, you can use an input value fr
       - order: "remove file {{ query }}"
     neurons:
       - shell:
-          cmd: "rm { query }}"
+          cmd: "rm {{ query }}"
           file_template: remove_file.j2          
 ```
 In the example above, kalliope will remove the file you asked for in the query.