Adjusting voicemails state_machine so that it will intelligently respond to missed...
[theonering] / src / gvoice / state_machine.py
1 #!/usr/bin/env python
2
3 import logging
4
5 import gobject
6
7 import util.go_utils as gobject_utils
8 import util.coroutines as coroutines
9 import gtk_toolbox
10
11
12 _moduleLogger = logging.getLogger("gvoice.state_machine")
13
14
15 def to_milliseconds(**kwd):
16         if "milliseconds" in kwd:
17                 return kwd["milliseconds"]
18         elif "seconds" in kwd:
19                 return kwd["seconds"] * 1000
20         elif "minutes" in kwd:
21                 return kwd["minutes"] * 1000 * 60
22         elif "hours" in kwd:
23                 return kwd["hours"] * 1000 * 60 * 60
24         raise KeyError("Unknown arg: %r" % kwd)
25
26
27 def to_seconds(**kwd):
28         if "milliseconds" in kwd:
29                 return kwd["milliseconds"] / 1000
30         elif "seconds" in kwd:
31                 return kwd["seconds"]
32         elif "minutes" in kwd:
33                 return kwd["minutes"] * 60
34         elif "hours" in kwd:
35                 return kwd["hours"] * 60 * 60
36         raise KeyError("Unknown arg: %r" % kwd)
37
38
39 class NopStateStrategy(object):
40
41         def __init__(self):
42                 pass
43
44         def initialize_state(self):
45                 pass
46
47         def reinitialize_state(self):
48                 pass
49
50         def increment_state(self):
51                 pass
52
53         @property
54         def timeout(self):
55                 return UpdateStateMachine.INFINITE_PERIOD
56
57         def __repr__(self):
58                 return "NopStateStrategy()"
59
60
61 class ConstantStateStrategy(object):
62
63         def __init__(self, timeout):
64                 assert 0 < timeout or timeout == UpdateStateMachine.INFINITE_PERIOD
65                 self._timeout = timeout
66
67         def initialize_state(self):
68                 pass
69
70         def reinitialize_state(self):
71                 pass
72
73         def increment_state(self):
74                 pass
75
76         @property
77         def timeout(self):
78                 return self._timeout
79
80         def __repr__(self):
81                 return "ConstantStateStrategy(timeout=%r)" % self._timeout
82
83
84 class NTimesStateStrategy(object):
85
86         def __init__(self, timeouts, postTimeout):
87                 assert 0 < len(timeouts)
88                 for timeout in timeouts:
89                         assert 0 < timeout or timeout == UpdateStateMachine.INFINITE_PERIOD
90                 assert 0 < postTimeout or postTimeout == UpdateStateMachine.INFINITE_PERIOD
91                 self._timeouts = timeouts
92                 self._postTimeout = postTimeout
93
94                 self._attemptCount = 0
95
96         def initialize_state(self):
97                 self._attemptCount = len(self._timeouts)
98
99         def reinitialize_state(self):
100                 self._attemptCount = 0
101
102         def increment_state(self):
103                 self._attemptCount += 1
104
105         @property
106         def timeout(self):
107                 try:
108                         return self._timeouts[self._attemptCount]
109                 except IndexError:
110                         return self._postTimeout
111
112         def __repr__(self):
113                 return "NTimesStateStrategy(timeouts=%r, postTimeout=%r)" % (
114                         self._timeouts,
115                         self._postTimeout,
116                 )
117
118
119 class GeometricStateStrategy(object):
120
121         def __init__(self, init, min, max):
122                 assert 0 < init and init < max or init == UpdateStateMachine.INFINITE_PERIOD
123                 assert 0 < min or min == UpdateStateMachine.INFINITE_PERIOD
124                 assert min < max or max == UpdateStateMachine.INFINITE_PERIOD
125                 self._min = min
126                 self._max = max
127                 self._init = init
128                 self._current = 0
129
130         def initialize_state(self):
131                 self._current = self._max
132
133         def reinitialize_state(self):
134                 self._current = self._min
135
136         def increment_state(self):
137                 if self._current == UpdateStateMachine.INFINITE_PERIOD:
138                         pass
139                 if self._init == UpdateStateMachine.INFINITE_PERIOD:
140                         self._current = UpdateStateMachine.INFINITE_PERIOD
141                 elif self._max == UpdateStateMachine.INFINITE_PERIOD:
142                         self._current *= 2
143                 else:
144                         self._current = min(2 * self._current, self._max - self._init)
145
146         @property
147         def timeout(self):
148                 if UpdateStateMachine.INFINITE_PERIOD in (self._init, self._current):
149                         timeout = UpdateStateMachine.INFINITE_PERIOD
150                 else:
151                         timeout = self._init + self._current
152                 return timeout
153
154         def __repr__(self):
155                 return "GeometricStateStrategy(init=%r, min=%r, max=%r)" % (
156                         self._init, self._min, self._max
157                 )
158
159
160 class StateMachine(object):
161
162         STATE_ACTIVE = 0, "active"
163         STATE_IDLE = 1, "idle"
164         STATE_DND = 2, "dnd"
165
166         def start(self):
167                 raise NotImplementedError("Abstract")
168
169         def stop(self):
170                 raise NotImplementedError("Abstract")
171
172         def close(self):
173                 raise NotImplementedError("Abstract")
174
175         def set_state(self, state):
176                 raise NotImplementedError("Abstract")
177
178         @property
179         def state(self):
180                 raise NotImplementedError("Abstract")
181
182
183 class MasterStateMachine(StateMachine):
184
185         def __init__(self):
186                 self._machines = []
187                 self._state = self.STATE_ACTIVE
188
189         def append_machine(self, machine):
190                 self._machines.append(machine)
191
192         def start(self):
193                 # Confirm we are all on the same page
194                 for machine in self._machines:
195                         machine.set_state(self._state)
196                 for machine in self._machines:
197                         machine.start()
198
199         def stop(self):
200                 for machine in self._machines:
201                         machine.stop()
202
203         def close(self):
204                 for machine in self._machines:
205                         machine.close()
206
207         def set_state(self, state):
208                 self._state = state
209                 for machine in self._machines:
210                         machine.set_state(state)
211
212         @property
213         def state(self):
214                 return self._state
215
216
217 class UpdateStateMachine(StateMachine):
218         # Making sure the it is initialized is finicky, be careful
219
220         INFINITE_PERIOD = -1
221         DEFAULT_MAX_TIMEOUT = to_seconds(hours=24)
222
223         _IS_DAEMON = True
224
225         def __init__(self, updateItems, name="", maxTime = DEFAULT_MAX_TIMEOUT):
226                 self._name = name
227                 self._updateItems = updateItems
228                 self._maxTime = maxTime
229
230                 self._state = self.STATE_ACTIVE
231                 self._timeoutId = None
232
233                 self._strategies = {}
234                 self._callback = coroutines.func_sink(
235                         coroutines.expand_positional(
236                                 self._request_reset_timers
237                         )
238                 )
239
240         def __repr__(self):
241                 return """UpdateStateMachine(
242         name=%r,
243         strategie=%r,
244 )""" % (self._name, self._strategies)
245
246         def set_state_strategy(self, state, strategy):
247                 self._strategies[state] = strategy
248
249         def start(self):
250                 assert self._timeoutId is None
251                 for strategy in self._strategies.itervalues():
252                         strategy.initialize_state()
253                 if self._strategy.timeout != self.INFINITE_PERIOD:
254                         self._timeoutId = gobject.idle_add(self._on_timeout)
255                 _moduleLogger.info("%s Starting State Machine" % (self._name, ))
256
257         def stop(self):
258                 _moduleLogger.info("%s Stopping State Machine" % (self._name, ))
259                 self._stop_update()
260
261         def close(self):
262                 assert self._timeoutId is None
263                 self._callback = None
264
265         def set_state(self, newState):
266                 if self._state == newState:
267                         return
268                 oldState = self._state
269                 _moduleLogger.info("%s Transitioning from %s to %s" % (self._name, oldState, newState))
270
271                 self._state = newState
272                 self._reset_timers(initialize=True)
273
274         @property
275         def state(self):
276                 return self._state
277
278         def reset_timers(self):
279                 self._reset_timers()
280
281         @property
282         def request_reset_timers(self):
283                 return self._callback
284
285         @property
286         def _strategy(self):
287                 return self._strategies[self._state]
288
289         @property
290         def maxTime(self):
291                 return self._maxTime
292
293         @gtk_toolbox.log_exception(_moduleLogger)
294         def _request_reset_timers(self, *args):
295                 self._reset_timers()
296
297         def _reset_timers(self, initialize=False):
298                 if self._timeoutId is None:
299                         return # not started yet
300                 _moduleLogger.info("%s Resetting State Machine" % (self._name, ))
301                 self._stop_update()
302                 if initialize:
303                         self._strategy.initialize_state()
304                 else:
305                         self._strategy.reinitialize_state()
306                 self._schedule_update()
307
308         def _stop_update(self):
309                 if self._timeoutId is None:
310                         return
311                 gobject.source_remove(self._timeoutId)
312                 self._timeoutId = None
313
314         def _schedule_update(self):
315                 assert self._timeoutId is None
316                 self._strategy.increment_state()
317                 nextTimeout = self._strategy.timeout
318                 if nextTimeout != self.INFINITE_PERIOD and nextTimeout < self._maxTime:
319                         assert 0 < nextTimeout
320                         self._timeoutId = gobject_utils.timeout_add_seconds(nextTimeout, self._on_timeout)
321                         _moduleLogger.info("%s Next update in %s seconds" % (self._name, nextTimeout, ))
322                 else:
323                         _moduleLogger.info("%s No further updates (timeout is %s seconds)" % (self._name, nextTimeout, ))
324
325         @gtk_toolbox.log_exception(_moduleLogger)
326         def _on_timeout(self):
327                 self._timeoutId = None
328                 self._schedule_update()
329                 for item in self._updateItems:
330                         try:
331                                 item.update(force=True)
332                         except Exception:
333                                 _moduleLogger.exception("Update failed for %r" % item)
334                 return False # do not continue