Lots more bug fixes
[theonering] / src / gvoice / state_machine.py
1 #!/usr/bin/env python
2
3 """
4 @todo Look into switching from POLL_TIME = min(F * 2^n, MAX) to POLL_TIME = min(CONST + F * 2^n, MAX)
5 @todo Look into supporting more states that have a different F and MAX
6 """
7
8 import Queue
9 import logging
10
11 import gobject
12
13 import util.algorithms as algorithms
14 import util.go_utils as gobject_utils
15 import util.coroutines as coroutines
16 import gtk_toolbox
17
18
19 _moduleLogger = logging.getLogger("gvoice.state_machine")
20
21
22 def _to_milliseconds(**kwd):
23         if "milliseconds" in kwd:
24                 return kwd["milliseconds"]
25         elif "seconds" in kwd:
26                 return kwd["seconds"] * 1000
27         elif "minutes" in kwd:
28                 return kwd["minutes"] * 1000 * 60
29         raise KeyError("Unknown arg: %r" % kwd)
30
31
32 class StateMachine(object):
33
34         STATE_ACTIVE = "active"
35         STATE_IDLE = "idle"
36         STATE_DND = "dnd"
37
38         _ACTION_UPDATE = "update"
39         _ACTION_RESET = "reset"
40         _ACTION_STOP = "stop"
41
42         _INITIAL_ACTIVE_PERIOD = int(_to_milliseconds(seconds=5))
43         _FINAL_ACTIVE_PERIOD = int(_to_milliseconds(minutes=2))
44         _IDLE_PERIOD = int(_to_milliseconds(minutes=10))
45         _INFINITE_PERIOD = -1
46
47         _IS_DAEMON = True
48
49         def __init__(self, initItems, updateItems):
50                 self._initItems = initItems
51                 self._updateItems = updateItems
52
53                 self._state = self.STATE_ACTIVE
54                 self._timeoutId = None
55                 self._currentPeriod = self._INITIAL_ACTIVE_PERIOD
56                 self._set_initial_period()
57
58                 self._callback = coroutines.func_sink(
59                         coroutines.expand_positional(
60                                 self._request_reset_timers
61                         )
62                 )
63
64         @gobject_utils.async
65         @gtk_toolbox.log_exception(_moduleLogger)
66         def start(self):
67                 _moduleLogger.info("Starting State Machine")
68                 for item in self._initItems:
69                         try:
70                                 item.update()
71                         except Exception:
72                                 _moduleLogger.exception("Initial update failed for %r" % item)
73                 self._schedule_update()
74
75         def stop(self):
76                 _moduleLogger.info("Stopping an already stopped state machine")
77                 self._stop_update()
78
79         def set_state(self, state):
80                 self._state = state
81                 self.reset_timers()
82
83         def get_state(self):
84                 return self._state
85
86         def reset_timers(self):
87                 self._reset_timers()
88
89         @property
90         def request_reset_timers(self):
91                 return self._callback
92
93         @gobject_utils.async
94         @gtk_toolbox.log_exception(_moduleLogger)
95         def _request_reset_timers(self, *args):
96                 self.reset_timers()
97
98         def _set_initial_period(self):
99                 self._currentPeriod = self._INITIAL_ACTIVE_PERIOD / 2 # We will double it later
100
101         def _schedule_update(self):
102                 nextTimeout = self._calculate_step(self._state, self._currentPeriod)
103                 nextTimeout = int(nextTimeout)
104                 if nextTimeout != self._INFINITE_PERIOD:
105                         self._timeoutId = gobject.timeout_add(nextTimeout, self._on_timeout)
106                 self._currentPeriod = nextTimeout
107
108         def _stop_update(self):
109                 if self._timeoutId is None:
110                         return
111                 gobject.source_remove(self._timeoutId)
112                 self._timeoutId = None
113
114         def _reset_timers(self):
115                 self._stop_update()
116                 self._set_initial_period()
117                 self._schedule_update()
118
119         def _on_timeout(self):
120                 _moduleLogger.info("Update")
121                 for item in self._updateItems:
122                         try:
123                                 item.update(force=True)
124                         except Exception:
125                                 _moduleLogger.exception("Update failed for %r" % item)
126                 self._schedule_update()
127                 return False # do not continue
128
129         @classmethod
130         def _calculate_step(cls, state, period):
131                 if state == cls.STATE_ACTIVE:
132                         return min(period * 2, cls._FINAL_ACTIVE_PERIOD)
133                 elif state == cls.STATE_IDLE:
134                         return cls._IDLE_PERIOD
135                 elif state == cls.STATE_DND:
136                         return cls._INFINITE_PERIOD
137                 else:
138                         raise RuntimeError("Unknown state: %r" % (state, ))