Python Library State Machine
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. #
  4. """
  5. state_machine (State Machine)
  6. =============================
  7. **Author:**
  8. * Dirk Alders <sudo-dirk@mount-mockery.de>
  9. **Description:**
  10. This Module helps implementing state machines.
  11. **Submodules:**
  12. * :class:`state_machine.state_machine`
  13. **Unittest:**
  14. See also the :download:`unittest <state_machine/_testresults_/unittest.pdf>` documentation.
  15. **Module Documentation:**
  16. """
  17. __DEPENDENCIES__ = []
  18. import logging
  19. import time
  20. logger_name = 'STATE_MACHINE'
  21. logger = logging.getLogger(logger_name)
  22. __INTERPRETER__ = (2, 3)
  23. """The supported Interpreter-Versions"""
  24. __DESCRIPTION__ = """This Module helps implementing state machines."""
  25. """The Module description"""
  26. class state_machine(object):
  27. """
  28. :param default_state: The default state which is set on initialisation.
  29. :param log_lvl: The log level, this Module logs to (see Loging-Levels of Module :mod:`logging`)
  30. .. note:: Additional keyword parameters well be stored as varibles of the instance (e.g. to give variables or methods for transition condition calculation).
  31. A state machine class can be created by deriving it from this class. The transitions are defined by overriding the variable `TRANSITIONS`.
  32. This Variable is a dictionary, where the key is the start-state and the content is a tuple or list of transitions. Each transition is a tuple or list
  33. including the following information: (condition-method (str), transition-time (number), target_state (str)).
  34. .. note:: The condition-method needs to be implemented as part of the new class.
  35. .. note:: It is usefull to define the states as variables of this class.
  36. **Example:**
  37. .. literalinclude:: ../examples/example.py
  38. .. literalinclude:: ../examples/example.log
  39. """
  40. TRANSITIONS = {}
  41. LOG_PREFIX = 'StateMachine:'
  42. def __init__(self, default_state, log_lvl, **kwargs):
  43. self.__state__ = None
  44. self.__last_transition_condition__ = None
  45. self.__conditions_start_time__ = {}
  46. self.__state_change_callbacks__ = {}
  47. self.__log_lvl__ = log_lvl
  48. self.__set_state__(default_state, '__init__')
  49. self.__callback_id__ = 0
  50. for key in kwargs:
  51. setattr(self, key, kwargs.get(key))
  52. def register_state_change_callback(self, state, condition, callback, *args, **kwargs):
  53. """
  54. :param state: The target state. The callback will be executed, if the state machine changes to this state. None means all states.
  55. :type state: str
  56. :param condition: The transition condition. The callback will be executed, if this condition is responsible for the state change. None means all conditions.
  57. :type condition: str
  58. :param callback: The callback to be executed.
  59. .. note:: Additional arguments and keyword parameters are supported. These arguments and parameters will be used as arguments and parameters for the callback execution.
  60. This methods allows to register callbacks which will be executed on state changes.
  61. """
  62. if state not in self.__state_change_callbacks__:
  63. self.__state_change_callbacks__[state] = {}
  64. if condition not in self.__state_change_callbacks__[state]:
  65. self.__state_change_callbacks__[state][condition] = []
  66. self.__state_change_callbacks__[state][condition].append((self.__callback_id__, callback, args, kwargs))
  67. self.__callback_id__ += 1
  68. def this_state(self):
  69. """
  70. :return: The current state.
  71. This method returns the current state of the state machine.
  72. """
  73. return self.__state__
  74. def this_state_is(self, state):
  75. """
  76. :param state: The state to be checked
  77. :type state: str
  78. :return: True if the given state is currently active, else False.
  79. :rtype: bool
  80. This methods returns the boolean information if the state machine is currently in the given state.
  81. """
  82. return self.__state__ == state
  83. def this_state_duration(self):
  84. """
  85. :return: The time how long the current state is active.
  86. :rtype: float
  87. This method returns the time how long the current state is active.
  88. """
  89. return time.time() - self.__time_stamp_state_change__
  90. def last_transition_condition(self):
  91. """
  92. :return: The last transition condition.
  93. :rtype: str
  94. This method returns the last transition condition.
  95. """
  96. return self.__last_transition_condition__
  97. def last_transition_condition_was(self, condition):
  98. """
  99. :param condition: The condition to be checked
  100. :type condition: str
  101. :return: True if the given condition was the last transition condition, else False.
  102. :rtype: bool
  103. This methods returns the boolean information if the last transition condition is equivalent to the given condition.
  104. """
  105. return self.__last_transition_condition__ == condition
  106. def previous_state(self):
  107. """
  108. :return: The previous state.
  109. :rtype: str
  110. This method returns the previous state of the state machine.
  111. """
  112. return self.__prev_state__
  113. def previous_state_was(self, state):
  114. """
  115. :param state: The state to be checked
  116. :type state: str
  117. :return: True if the given state was previously active, else False.
  118. :rtype: bool
  119. This methods returns the boolean information if the state machine was previously in the given state.
  120. """
  121. return self.__prev_state__ == state
  122. def previous_state_duration(self):
  123. """
  124. :return: The time how long the previous state was active.
  125. :rtype: float
  126. This method returns the time how long the previous state was active.
  127. """
  128. return self.__prev_state_dt__
  129. def __set_state__(self, target_state, condition):
  130. logger.log(self.__log_lvl__, "%s State change (%s): %s -> %s", self.LOG_PREFIX, repr(condition), repr(self.__state__), repr(target_state))
  131. timestamp = time.time()
  132. self.__prev_state__ = self.__state__
  133. if self.__prev_state__ is None:
  134. self.__prev_state_dt__ = 0.
  135. else:
  136. self.__prev_state_dt__ = timestamp - self.__time_stamp_state_change__
  137. self.__state__ = target_state
  138. self.__last_transition_condition__ = condition
  139. self.__time_stamp_state_change__ = timestamp
  140. self.__conditions_start_time__ = {}
  141. # Callback collect
  142. this_state_change_callbacks = self.__state_change_callbacks__.get(None, {}).get(None, [])
  143. this_state_change_callbacks.extend(self.__state_change_callbacks__.get(target_state, {}).get(None, []))
  144. this_state_change_callbacks.extend(self.__state_change_callbacks__.get(None, {}).get(condition, []))
  145. this_state_change_callbacks.extend(self.__state_change_callbacks__.get(target_state, {}).get(condition, []))
  146. # Callback sorting
  147. this_state_change_callbacks.sort()
  148. # Callback execution
  149. for cid, callback, args, kwargs in this_state_change_callbacks:
  150. logger.debug('Executing callback %d - %s.%s', cid, callback.__module__, callback.__name__)
  151. callback(*args, **kwargs)
  152. def work(self):
  153. """
  154. This Method needs to be executed cyclicly to enable the state machine.
  155. """
  156. tm = time.time()
  157. transitions = self.TRANSITIONS.get(self.this_state())
  158. if transitions is not None:
  159. active_transitions = []
  160. cnt = 0
  161. for method_name, transition_delay, target_state in transitions:
  162. method = getattr(self, method_name)
  163. if method():
  164. if method_name not in self.__conditions_start_time__:
  165. self.__conditions_start_time__[method_name] = tm
  166. if tm - self.__conditions_start_time__[method_name] >= transition_delay:
  167. active_transitions.append((transition_delay - tm + self.__conditions_start_time__[method_name], cnt, target_state, method_name))
  168. else:
  169. self.__conditions_start_time__[method_name] = tm
  170. cnt += 1
  171. if len(active_transitions) > 0:
  172. active_transitions.sort()
  173. self.__set_state__(active_transitions[0][2], active_transitions[0][3])