abstract away ScheduledFunction
[~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 .gobject import ScheduledFunction
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 = ScheduledFunction(delay, func)
104
105     def _cancel_transition(self):
106         assert self.transition is not None
107         self.transition.cancel()
108         self.transition = None
109
110     def changestate(self, state):
111         if state != ST_ACTIVE:
112             OnoffDevice.changestate(self, state)
113         else:
114             if self.desired_state == 0:
115                 logger.warning("device became active but we want inactive," +
116                                "suppresing signal")
117             elif self.transition is None:
118                 logger.debug("scheduling report of activation in %.1fs",
119                              self.ondelay)
120                 self._schedule_transition(self.ondelay, self._report_active)
121             else:
122                 logger.debug("suppressing duplicate activation signal")
123
124     def _report_active(self):
125         assert self.desired_state == ST_ACTIVE
126         assert self.transition is not None
127         self.transition = None
128         logger.debug("delivering activation signal")
129         OnoffDevice.changestate(self, ST_ACTIVE)
130
131     def activate(self):
132         if self.desired_state == 0 and self.transition is not None:
133             logger.debug("cancelling pending deactivation")
134             self._cancel_transition()
135             curstate = self.device.state
136             if curstate == ST_ACTIVE:
137                 self.desired_state = ST_ACTIVE
138                 OnoffDevice.changestate(self, ST_ACTIVE)
139                 return
140             logger.warning("device should be active during delayed " +
141                            "deactivation, but is in state %d", curstate)
142         self.desired_state = ST_ACTIVE
143         self.device.activate()
144         self.changestate(self.device.state)
145
146     def _do_stop(self):
147         assert self.desired_state == 0
148         assert self.transition is not None
149         self.transition = None
150         logger.debug("actually deactivating")
151         self.device.deactivate()
152
153     def deactivate(self):
154         if self.desired_state == ST_ACTIVE and self.transition is not None:
155             logger.debug("cancelling pending activation report")
156             self._cancel_transition()
157         self.desired_state = 0
158         if self.transition is None:
159             logger.debug("scheduling actual deactivate in %.1fs",
160                          self.offdelay)
161             self._schedule_transition(self.offdelay, self._do_stop)
162             self.changestate(self.state)
163         else:
164             logger.debug("not issuing deactivate due to pending deactivate")
165
166     def close(self):
167         if self.transition is not None:
168             logger.info("cancelling pending transition")
169             self._cancel_transition()
170             if self.desired_state == 0:
171                 logger.info("invoking pending deactivate early during close")
172                 self.device.deactivate()
173         self.device.notify.remove(self.changestate)
174         self.device.close()