Switching timeouts to seconds for whenever platforms support it
[theonering] / src / gvoice / state_machine.py
1 #!/usr/bin/env python
2
3 """
4 @todo Look into supporting more states
5 """
6
7 import logging
8
9 import gobject
10
11 import util.go_utils as gobject_utils
12 import util.coroutines as coroutines
13 import gtk_toolbox
14
15
16 _moduleLogger = logging.getLogger("gvoice.state_machine")
17
18
19 def to_milliseconds(**kwd):
20         if "milliseconds" in kwd:
21                 return kwd["milliseconds"]
22         elif "seconds" in kwd:
23                 return kwd["seconds"] * 1000
24         elif "minutes" in kwd:
25                 return kwd["minutes"] * 1000 * 60
26         elif "hours" in kwd:
27                 return kwd["hours"] * 1000 * 60 * 60
28         raise KeyError("Unknown arg: %r" % kwd)
29
30
31 def to_seconds(**kwd):
32         if "milliseconds" in kwd:
33                 return kwd["milliseconds"] / 1000
34         elif "seconds" in kwd:
35                 return kwd["seconds"]
36         elif "minutes" in kwd:
37                 return kwd["minutes"] * 60
38         elif "hours" in kwd:
39                 return kwd["hours"] * 60 * 60
40         raise KeyError("Unknown arg: %r" % kwd)
41
42
43 class NopStateStrategy(object):
44
45         def __init__(self):
46                 pass
47
48         def initialize_state(self):
49                 pass
50
51         def increment_state(self):
52                 pass
53
54         @property
55         def timeout(self):
56                 return UpdateStateMachine.INFINITE_PERIOD
57
58
59 class ConstantStateStrategy(object):
60
61         def __init__(self, timeout):
62                 assert 0 < timeout or timeout == UpdateStateMachine.INFINITE_PERIOD
63                 self._timeout = timeout
64
65         def initialize_state(self):
66                 pass
67
68         def increment_state(self):
69                 pass
70
71         @property
72         def timeout(self):
73                 return self._timeout
74
75
76 class GeometricStateStrategy(object):
77
78         def __init__(self, init, min, max):
79                 assert 0 < init and init < max and init != UpdateStateMachine.INFINITE_PERIOD
80                 assert 0 < min and min != UpdateStateMachine.INFINITE_PERIOD
81                 assert min < max or max == UpdateStateMachine.INFINITE_PERIOD
82                 self._min = min
83                 self._max = max
84                 self._init = init
85                 self._current = 0
86
87         def initialize_state(self):
88                 self._current = self._min / 2
89
90         def increment_state(self):
91                 if self._max == UpdateStateMachine.INFINITE_PERIOD:
92                         self._current *= 2
93                 else:
94                         self._current = min(2 * self._current, self._max - self._init)
95
96         @property
97         def timeout(self):
98                 return self._init + self._current
99
100
101 class StateMachine(object):
102
103         STATE_ACTIVE = 0, "active"
104         STATE_IDLE = 1, "idle"
105         STATE_DND = 2, "dnd"
106
107         def start(self):
108                 raise NotImplementedError("Abstract")
109
110         def stop(self):
111                 raise NotImplementedError("Abstract")
112
113         def close(self):
114                 raise NotImplementedError("Abstract")
115
116         def set_state(self, state):
117                 raise NotImplementedError("Abstract")
118
119         @property
120         def state(self):
121                 raise NotImplementedError("Abstract")
122
123
124 class MasterStateMachine(StateMachine):
125
126         def __init__(self):
127                 self._machines = []
128                 self._state = self.STATE_ACTIVE
129
130         def append_machine(self, machine):
131                 self._machines.append(machine)
132
133         def start(self):
134                 # Confirm we are all on the same page
135                 for machine in self._machines:
136                         machine.set_state(self._state)
137                 for machine in self._machines:
138                         machine.start()
139
140         def stop(self):
141                 for machine in self._machines:
142                         machine.stop()
143
144         def close(self):
145                 for machine in self._machines:
146                         machine.close()
147
148         def set_state(self, state):
149                 self._state = state
150                 for machine in self._machines:
151                         machine.set_state(state)
152
153         @property
154         def state(self):
155                 return self._state
156
157
158 class UpdateStateMachine(StateMachine):
159         # Making sure the it is initialized is finicky, be careful
160
161         INFINITE_PERIOD = -1
162
163         _IS_DAEMON = True
164
165         def __init__(self, updateItems, name=""):
166                 self._name = name
167                 self._updateItems = updateItems
168
169                 self._state = self.STATE_ACTIVE
170                 self._timeoutId = None
171
172                 self._strategies = {}
173                 self._callback = coroutines.func_sink(
174                         coroutines.expand_positional(
175                                 self._request_reset_timers
176                         )
177                 )
178
179         def set_state_strategy(self, state, strategy):
180                 self._strategies[state] = strategy
181
182         def start(self):
183                 assert self._timeoutId is None
184                 for strategy in self._strategies.itervalues():
185                         strategy.initialize_state()
186                 if self._strategy.timeout != self.INFINITE_PERIOD:
187                         self._timeoutId = gobject.idle_add(self._on_timeout)
188                 _moduleLogger.info("%s Starting State Machine" % (self._name, ))
189
190         def stop(self):
191                 _moduleLogger.info("%s Stopping State Machine" % (self._name, ))
192                 self._stop_update()
193
194         def close(self):
195                 assert self._timeoutId is None
196                 self._callback = None
197
198         def set_state(self, newState):
199                 if self._state == newState:
200                         return
201                 oldState = self._state
202                 _moduleLogger.info("%s Transitioning from %s to %s" % (self._name, oldState, newState))
203
204                 self._state = newState
205                 self._reset_timers()
206
207         @property
208         def state(self):
209                 return self._state
210
211         def reset_timers(self):
212                 _moduleLogger.info("%s Resetting State Machine" % (self._name, ))
213                 self._reset_timers()
214
215         @property
216         def request_reset_timers(self):
217                 return self._callback
218
219         @property
220         def _strategy(self):
221                 return self._strategies[self._state]
222
223         @gtk_toolbox.log_exception(_moduleLogger)
224         def _request_reset_timers(self, *args):
225                 self._reset_timers()
226
227         def _schedule_update(self):
228                 assert self._timeoutId is None
229                 self._strategy.increment_state()
230                 nextTimeout = self._strategy.timeout
231                 if nextTimeout != self.INFINITE_PERIOD:
232                         self._timeoutId = gobject_utils.timeout_add_seconds(nextTimeout, self._on_timeout)
233                 _moduleLogger.info("%s Next update in %s ms" % (self._name, nextTimeout, ))
234
235         def _stop_update(self):
236                 if self._timeoutId is None:
237                         return
238                 gobject.source_remove(self._timeoutId)
239                 self._timeoutId = None
240
241         def _reset_timers(self):
242                 if self._timeoutId is None:
243                         return # not started yet
244                 self._stop_update()
245                 self._strategy.initialize_state()
246                 self._schedule_update()
247
248         @gtk_toolbox.log_exception(_moduleLogger)
249         def _on_timeout(self):
250                 self._timeoutId = None
251                 self._schedule_update()
252                 for item in self._updateItems:
253                         try:
254                                 item.update(force=True)
255                         except Exception:
256                                 _moduleLogger.exception("Update failed for %r" % item)
257                 return False # do not continue