Python Library State Machine
Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

__init__.py 8.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  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. for key in kwargs:
  50. setattr(self, key, kwargs.get(key))
  51. def register_state_change_callback(self, state, condition, callback, *args, **kwargs):
  52. """
  53. :param state: The target state. The callback will be executed, if the state machine changes to this state. None means all states.
  54. :type state: str
  55. :param condition: The transition condition. The callback will be executed, if this condition is responsible for the state change. None means all conditions.
  56. :type condition: str
  57. :param callback: The callback to be executed.
  58. .. note:: Additional arguments and keyword parameters are supported. These arguments and parameters will be used as arguments and parameters for the callback execution.
  59. This methods allows to register callbacks which will be executed on state changes.
  60. """
  61. if state not in self.__state_change_callbacks__:
  62. self.__state_change_callbacks__[state] = {}
  63. if condition not in self.__state_change_callbacks__[state]:
  64. self.__state_change_callbacks__[state][condition] = []
  65. self.__state_change_callbacks__[state][condition].append((callback, args, kwargs))
  66. def this_state(self):
  67. """
  68. :return: The current state.
  69. This method returns the current state of the state machine.
  70. """
  71. return self.__state__
  72. def this_state_is(self, state):
  73. """
  74. :param state: The state to be checked
  75. :type state: str
  76. :return: True if the given state is currently active, else False.
  77. :rtype: bool
  78. This methods returns the boolean information if the state machine is currently in the given state.
  79. """
  80. return self.__state__ == state
  81. def this_state_duration(self):
  82. """
  83. :return: The time how long the current state is active.
  84. :rtype: float
  85. This method returns the time how long the current state is active.
  86. """
  87. return time.time() - self.__time_stamp_state_change__
  88. def last_transition_condition(self):
  89. """
  90. :return: The last transition condition.
  91. :rtype: str
  92. This method returns the last transition condition.
  93. """
  94. return self.__last_transition_condition__
  95. def last_transition_condition_was(self, condition):
  96. """
  97. :param condition: The condition to be checked
  98. :type condition: str
  99. :return: True if the given condition was the last transition condition, else False.
  100. :rtype: bool
  101. This methods returns the boolean information if the last transition condition is equivalent to the given condition.
  102. """
  103. return self.__last_transition_condition__ == condition
  104. def previous_state(self):
  105. """
  106. :return: The previous state.
  107. :rtype: str
  108. This method returns the previous state of the state machine.
  109. """
  110. return self.__prev_state__
  111. def previous_state_was(self, state):
  112. """
  113. :param state: The state to be checked
  114. :type state: str
  115. :return: True if the given state was previously active, else False.
  116. :rtype: bool
  117. This methods returns the boolean information if the state machine was previously in the given state.
  118. """
  119. return self.__prev_state__ == state
  120. def previous_state_duration(self):
  121. """
  122. :return: The time how long the previous state was active.
  123. :rtype: float
  124. This method returns the time how long the previous state was active.
  125. """
  126. return self.__prev_state_dt__
  127. def __set_state__(self, target_state, condition):
  128. logger.log(self.__log_lvl__, "%s State change (%s): %s -> %s", self.LOG_PREFIX, repr(condition), repr(self.__state__), repr(target_state))
  129. timestamp = time.time()
  130. self.__prev_state__ = self.__state__
  131. if self.__prev_state__ is None:
  132. self.__prev_state_dt__ = 0.
  133. else:
  134. self.__prev_state_dt__ = timestamp - self.__time_stamp_state_change__
  135. self.__state__ = target_state
  136. self.__last_transition_condition__ = condition
  137. self.__time_stamp_state_change__ = timestamp
  138. self.__conditions_start_time__ = {}
  139. for callback, args, kwargs in self.__state_change_callbacks__.get(None, {}).get(None, []):
  140. callback(*args, **kwargs)
  141. for callback, args, kwargs in self.__state_change_callbacks__.get(target_state, {}).get(None, []):
  142. callback(*args, **kwargs)
  143. for callback, args, kwargs in self.__state_change_callbacks__.get(None, {}).get(condition, []):
  144. callback(*args, **kwargs)
  145. for callback, args, kwargs in self.__state_change_callbacks__.get(target_state, {}).get(condition, []):
  146. callback(*args, **kwargs)
  147. def work(self):
  148. """
  149. This Method needs to be executed cyclicly to enable the state machine.
  150. """
  151. tm = time.time()
  152. transitions = self.TRANSITIONS.get(self.this_state())
  153. if transitions is not None:
  154. active_transitions = []
  155. cnt = 0
  156. for method_name, transition_delay, target_state in transitions:
  157. method = getattr(self, method_name)
  158. if method():
  159. if method_name not in self.__conditions_start_time__:
  160. self.__conditions_start_time__[method_name] = tm
  161. if tm - self.__conditions_start_time__[method_name] >= transition_delay:
  162. active_transitions.append((transition_delay - tm + self.__conditions_start_time__[method_name], cnt, target_state, method_name))
  163. else:
  164. self.__conditions_start_time__[method_name] = tm
  165. cnt += 1
  166. if len(active_transitions) > 0:
  167. active_transitions.sort()
  168. self.__set_state__(active_transitions[0][2], active_transitions[0][3])