ThrottledDevice: speed up quick reactivation
[~helmut/onoff.git] / onoff / common.py
index 26e32bf..4a1fe8c 100644 (file)
 """
 Defined states:
* 0: The device is inactive.
ST_TRANSITION: The device is transitioning from inactive to active.
* ST_ACTIVE: The device is active.
* ST_ACTIVE|ST_TRANSITION: The device is transitioning from active to inactive.
- 0: The device is inactive.
- ST_ACTIVE|ST_TRANSITION: The device is transitioning from inactive to active.
- ST_ACTIVE: The device is active.
ST_TRANSITION: The device is transitioning from active to inactive.
 """
 
+import logging
+
+from gi.repository import GObject
+
+logger = logging.getLogger("onoff.common")
+
 ST_ACTIVE = 1
 ST_TRANSITION = 2
+
+class OnoffDevice(object):
+    """A device is a thing with two states, that can be asked to transition
+    from either state to the other. It can signal state changes to interested
+    parties.
+
+    @type notify: {int -> None}
+    @ivar notify: A set of functions taking a changed state.
+
+    @type state: int
+    @ivar state: is a read-only attribute to retrieve the current state
+    """
+    def __init__(self):
+        self.notify = set()
+
+    def changestate(self, state):
+        """Tell interested parties that the state has changed to the given
+        state."""
+        for func in self.notify:
+            func(state)
+
+    def activate(self):
+        """Ask the device to power on."""
+        raise NotImplementedError
+
+    def deactivate(self):
+        """Ask the device to power off."""
+        raise NotImplementedError
+
+    def close(self):
+        """Release resources acquired by the constructor."""
+        pass
+
+class InvertedDevice(OnoffDevice):
+    """A device that swaps active and inactive states of a give device."""
+    def __init__(self, device):
+        OnoffDevice.__init__(self)
+        self.device = device
+        self.device.activate()
+        self.device.notify.add(self.changestate)
+
+    def changestate(self, state):
+        OnoffDevice.changestate(self, state ^ ST_ACTIVE)
+
+    @property
+    def state(self):
+        return self.device.state ^ ST_ACTIVE
+
+    def activate(self):
+        self.device.deactivate()
+
+    def deactivate(self):
+        self.device.activate()
+
+    def close(self):
+        self.device.notify.remove(self.changestate)
+        self.device.close()
+
+class ThrottledDevice(OnoffDevice):
+    """A device that delays the activation signal and the actual deactivation
+    by a fixed amounts of time. This limits the rate of state transitions to
+    two per offdelay."""
+    def __init__(self, device, ondelay, offdelay):
+        """
+        @type device: OnoffDevice
+        @type ondelay: int or float
+        @param ondelay: delay the report of ST_ACTIVE by this many seconds
+        @type offdelay: int or float
+        @param offdelay: delay the actual deactivation by this many seconds
+        """
+        OnoffDevice.__init__(self)
+        self.device = device
+        self.ondelay = ondelay
+        self.offdelay = offdelay
+        self.desired_state = 0
+        self.transition = None
+        self.device.notify.add(self.changestate)
+
+    @property
+    def state(self):
+        if self.transition is None:
+            return self.device.state
+        return self.desired_state | ST_TRANSITION
+
+    def _schedule_transition(self, delay, func):
+        assert self.transition is None
+        self.transition = GObject.timeout_add(int(1000 * delay), func)
+
+    def _cancel_transition(self):
+        assert self.transition is not None
+        ret = GObject.source_remove(self.transition)
+        assert ret
+        self.transition = None
+
+    def changestate(self, state):
+        if state != ST_ACTIVE:
+            OnoffDevice.changestate(self, state)
+        else:
+            if self.desired_state == 0:
+                logger.warn("device became active but we want inactive," +
+                            "suppresing signal")
+            elif self.transition is None:
+                logger.debug("scheduling report of activation in %.1fs",
+                             self.ondelay)
+                self._schedule_transition(self.ondelay, self._report_active)
+            else:
+                logger.debug("suppressing duplicate activation signal")
+
+    def _report_active(self):
+        assert self.desired_state == ST_ACTIVE
+        assert self.transition is not None
+        self.transition = None
+        logger.debug("delivering activation signal")
+        OnoffDevice.changestate(self, ST_ACTIVE)
+
+    def activate(self):
+        if self.desired_state == 0 and self.transition is not None:
+            logger.debug("cancelling pending deactivation")
+            self._cancel_transition()
+            curstate = self.device.state
+            if curstate == ST_ACTIVE:
+                self.desired_state = ST_ACTIVE
+                OnoffDevice.changestate(self, ST_ACTIVE)
+                return
+            logger.warn("device should be active during delayed deactivation" +
+                        ", but is in state %d", curstate)
+        self.desired_state = ST_ACTIVE
+        self.device.activate()
+        self.changestate(self.device.state)
+
+    def _do_stop(self):
+        assert self.desired_state == 0
+        assert self.transition is not None
+        self.transition = None
+        logger.debug("actually deactivating")
+        self.device.deactivate()
+
+    def deactivate(self):
+        if self.desired_state == ST_ACTIVE and self.transition is not None:
+            logger.debug("cancelling pending activation report")
+            self._cancel_transition()
+        self.desired_state = 0
+        if self.transition is None:
+            logger.debug("scheduling actual deactivate in %.1fs",
+                         self.offdelay)
+            self._schedule_transition(self.offdelay, self._do_stop)
+            self.changestate(self.state)
+        else:
+            logger.debug("not issuing deactivate due to pending deactivate")
+
+    def close(self):
+        if self.transition is not None:
+            logger.info("cancelling pending transition")
+            self._cancel_transition()
+            if self.desired_state == 0:
+                logger.info("invoking pending deactivate early during close")
+                self.device.deactivate()
+        self.device.notify.remove(self.changestate)
+        self.device.close()