Minor cleanup
[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 logging
9
10 import gobject
11
12 import util.go_utils as gobject_utils
13 import util.coroutines as coroutines
14 import gtk_toolbox
15
16
17 _moduleLogger = logging.getLogger("gvoice.state_machine")
18
19
20 def _to_milliseconds(**kwd):
21         if "milliseconds" in kwd:
22                 return kwd["milliseconds"]
23         elif "seconds" in kwd:
24                 return kwd["seconds"] * 1000
25         elif "minutes" in kwd:
26                 return kwd["minutes"] * 1000 * 60
27         raise KeyError("Unknown arg: %r" % kwd)
28
29
30 class StateMachine(object):
31
32         STATE_ACTIVE = 0, "active"
33         STATE_IDLE = 1, "idle"
34         STATE_DND = 2, "dnd"
35
36         _ACTION_UPDATE = "update"
37         _ACTION_RESET = "reset"
38         _ACTION_STOP = "stop"
39
40         _INITIAL_ACTIVE_PERIOD = int(_to_milliseconds(seconds=10))
41         _FINAL_ACTIVE_PERIOD = int(_to_milliseconds(minutes=10))
42         _IDLE_PERIOD = int(_to_milliseconds(minutes=30))
43         _INFINITE_PERIOD = -1
44
45         _IS_DAEMON = True
46
47         def __init__(self, initItems, updateItems):
48                 self._initItems = initItems
49                 self._updateItems = updateItems
50
51                 self._state = self.STATE_ACTIVE
52                 self._startId = None
53                 self._timeoutId = None
54                 self._currentPeriod = self._INITIAL_ACTIVE_PERIOD
55                 self._set_initial_period()
56
57                 self._callback = coroutines.func_sink(
58                         coroutines.expand_positional(
59                                 self._request_reset_timers
60                         )
61                 )
62
63         def close(self):
64                 self._callback = None
65
66         def start(self):
67                 assert self._startId is None
68                 assert self._timeoutId is None
69                 self._startId = gobject.idle_add(self._start)
70
71         def stop(self):
72                 if self._startId is not None:
73                         _moduleLogger.info("Stopping state machine before it even had a chance to start")
74                         gobject.source_remove(self._startId)
75                         self._startId = None
76                 self._stop_update()
77
78         def set_state(self, newState):
79                 oldState = self._state
80                 _moduleLogger.info("Transitioning from %s to %s" % (oldState, newState))
81
82                 self._state = newState
83                 self._reset_timers()
84
85         def get_state(self):
86                 return self._state
87
88         def reset_timers(self):
89                 self._reset_timers()
90
91         @property
92         def request_reset_timers(self):
93                 return self._callback
94
95         @gobject_utils.async
96         @gtk_toolbox.log_exception(_moduleLogger)
97         def _request_reset_timers(self, *args):
98                 self._reset_timers()
99
100         def _set_initial_period(self):
101                 self._currentPeriod = self._INITIAL_ACTIVE_PERIOD / 2 # We will double it later
102
103         def _schedule_update(self):
104                 assert self._timeoutId is None
105                 nextTimeout = self._calculate_step(self._state, self._currentPeriod)
106                 nextTimeout = int(nextTimeout)
107                 if nextTimeout != self._INFINITE_PERIOD:
108                         self._timeoutId = gobject.timeout_add(nextTimeout, self._on_timeout)
109                 _moduleLogger.info("Next update in %s ms" % (nextTimeout, ))
110                 self._currentPeriod = nextTimeout
111
112         def _start(self):
113                 _moduleLogger.info("Starting State Machine")
114                 for item in self._initItems:
115                         try:
116                                 item.update()
117                         except Exception:
118                                 _moduleLogger.exception("Initial update failed for %r" % item)
119                 self._schedule_update()
120                 self._startId = None
121                 return False # do not continue
122
123         def _stop_update(self):
124                 if self._timeoutId is None:
125                         _moduleLogger.info("Stopping an already stopped state machine")
126                         return
127                 gobject.source_remove(self._timeoutId)
128                 self._timeoutId = None
129
130         def _reset_timers(self):
131                 if self._timeoutId is None:
132                         return # not started yet
133                 self._stop_update()
134                 self._set_initial_period()
135                 self._schedule_update()
136
137         def _on_timeout(self):
138                 _moduleLogger.info("Update")
139                 for item in self._updateItems:
140                         try:
141                                 item.update(force=True)
142                         except Exception:
143                                 _moduleLogger.exception("Update failed for %r" % item)
144                 self._timeoutId = None
145                 self._schedule_update()
146                 return False # do not continue
147
148         @classmethod
149         def _calculate_step(cls, state, period):
150                 if state == cls.STATE_ACTIVE:
151                         return min(period * 2, cls._FINAL_ACTIVE_PERIOD)
152                 elif state == cls.STATE_IDLE:
153                         return cls._IDLE_PERIOD
154                 elif state == cls.STATE_DND:
155                         return cls._INFINITE_PERIOD
156                 else:
157                         raise RuntimeError("Unknown state: %r" % (state, ))