ThrottledDevice: speed up quick reactivation
[~helmut/onoff.git] / onoff / common.py
1 """
2 Defined states:
3  - 0: The device is inactive.
4  - ST_ACTIVE|ST_TRANSITION: The device is transitioning from inactive to active.
5  - ST_ACTIVE: The device is active.
6  - ST_TRANSITION: The device is transitioning from active to inactive.
7 """
8
9 import logging
10
11 from gi.repository import GObject
12
13 logger = logging.getLogger("onoff.common")
14
15 ST_ACTIVE = 1
16 ST_TRANSITION = 2
17
18 class OnoffDevice(object):
19     """A device is a thing with two states, that can be asked to transition
20     from either state to the other. It can signal state changes to interested
21     parties.
22
23     @type notify: {int -> None}
24     @ivar notify: A set of functions taking a changed state.
25
26     @type state: int
27     @ivar state: is a read-only attribute to retrieve the current state
28     """
29     def __init__(self):
30         self.notify = set()
31
32     def changestate(self, state):
33         """Tell interested parties that the state has changed to the given
34         state."""
35         for func in self.notify:
36             func(state)
37
38     def activate(self):
39         """Ask the device to power on."""
40         raise NotImplementedError
41
42     def deactivate(self):
43         """Ask the device to power off."""
44         raise NotImplementedError
45
46     def close(self):
47         """Release resources acquired by the constructor."""
48         pass
49
50 class InvertedDevice(OnoffDevice):
51     """A device that swaps active and inactive states of a give device."""
52     def __init__(self, device):
53         OnoffDevice.__init__(self)
54         self.device = device
55         self.device.activate()
56         self.device.notify.add(self.changestate)
57
58     def changestate(self, state):
59         OnoffDevice.changestate(self, state ^ ST_ACTIVE)
60
61     @property
62     def state(self):
63         return self.device.state ^ ST_ACTIVE
64
65     def activate(self):
66         self.device.deactivate()
67
68     def deactivate(self):
69         self.device.activate()
70
71     def close(self):
72         self.device.notify.remove(self.changestate)
73         self.device.close()
74
75 class ThrottledDevice(OnoffDevice):
76     """A device that delays the activation signal and the actual deactivation
77     by a fixed amounts of time. This limits the rate of state transitions to
78     two per offdelay."""
79     def __init__(self, device, ondelay, offdelay):
80         """
81         @type device: OnoffDevice
82         @type ondelay: int or float
83         @param ondelay: delay the report of ST_ACTIVE by this many seconds
84         @type offdelay: int or float
85         @param offdelay: delay the actual deactivation by this many seconds
86         """
87         OnoffDevice.__init__(self)
88         self.device = device
89         self.ondelay = ondelay
90         self.offdelay = offdelay
91         self.desired_state = 0
92         self.transition = None
93         self.device.notify.add(self.changestate)
94
95     @property
96     def state(self):
97         if self.transition is None:
98             return self.device.state
99         return self.desired_state | ST_TRANSITION
100
101     def _schedule_transition(self, delay, func):
102         assert self.transition is None
103         self.transition = GObject.timeout_add(int(1000 * delay), func)
104
105     def _cancel_transition(self):
106         assert self.transition is not None
107         ret = GObject.source_remove(self.transition)
108         assert ret
109         self.transition = None
110
111     def changestate(self, state):
112         if state != ST_ACTIVE:
113             OnoffDevice.changestate(self, state)
114         else:
115             if self.desired_state == 0:
116                 logger.warn("device became active but we want inactive," +
117                             "suppresing signal")
118             elif self.transition is None:
119                 logger.debug("scheduling report of activation in %.1fs",
120                              self.ondelay)
121                 self._schedule_transition(self.ondelay, self._report_active)
122             else:
123                 logger.debug("suppressing duplicate activation signal")
124
125     def _report_active(self):
126         assert self.desired_state == ST_ACTIVE
127         assert self.transition is not None
128         self.transition = None
129         logger.debug("delivering activation signal")
130         OnoffDevice.changestate(self, ST_ACTIVE)
131
132     def activate(self):
133         if self.desired_state == 0 and self.transition is not None:
134             logger.debug("cancelling pending deactivation")
135             self._cancel_transition()
136             curstate = self.device.state
137             if curstate == ST_ACTIVE:
138                 self.desired_state = ST_ACTIVE
139                 OnoffDevice.changestate(self, ST_ACTIVE)
140                 return
141             logger.warn("device should be active during delayed deactivation" +
142                         ", but is in state %d", curstate)
143         self.desired_state = ST_ACTIVE
144         self.device.activate()
145         self.changestate(self.device.state)
146
147     def _do_stop(self):
148         assert self.desired_state == 0
149         assert self.transition is not None
150         self.transition = None
151         logger.debug("actually deactivating")
152         self.device.deactivate()
153
154     def deactivate(self):
155         if self.desired_state == ST_ACTIVE and self.transition is not None:
156             logger.debug("cancelling pending activation report")
157             self._cancel_transition()
158         self.desired_state = 0
159         if self.transition is None:
160             logger.debug("scheduling actual deactivate in %.1fs",
161                          self.offdelay)
162             self._schedule_transition(self.offdelay, self._do_stop)
163             self.changestate(self.state)
164         else:
165             logger.debug("not issuing deactivate due to pending deactivate")
166
167     def close(self):
168         if self.transition is not None:
169             logger.info("cancelling pending transition")
170             self._cancel_transition()
171             if self.desired_state == 0:
172                 logger.info("invoking pending deactivate early during close")
173                 self.device.deactivate()
174         self.device.notify.remove(self.changestate)
175         self.device.close()