use the enum module to represent states
[~helmut/onoff.git] / onoff / common.py
index 7f389c9..5d9565c 100644 (file)
@@ -1,19 +1,18 @@
-"""
-Defined states:
- * 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 enum
 import logging
 
 import logging
 
-from gi.repository import GObject
+from .gobject import ScheduledFunction
 
 logger = logging.getLogger("onoff.common")
 
 
 logger = logging.getLogger("onoff.common")
 
-ST_ACTIVE = 1
-ST_TRANSITION = 2
+
+class OnoffState(enum.Flag):
+    inactive = 0
+    active = 1
+    deactivating = 2
+    activating = 3
+    transition = 2
+
 
 class OnoffDevice(object):
     """A device is a thing with two states, that can be asked to transition
 
 class OnoffDevice(object):
     """A device is a thing with two states, that can be asked to transition
@@ -56,11 +55,11 @@ class InvertedDevice(OnoffDevice):
         self.device.notify.add(self.changestate)
 
     def changestate(self, state):
         self.device.notify.add(self.changestate)
 
     def changestate(self, state):
-        OnoffDevice.changestate(self, state ^ ST_ACTIVE)
+        OnoffDevice.changestate(self, state ^ OnoffState.active)
 
     @property
     def state(self):
 
     @property
     def state(self):
-        return self.device.state ^ ST_ACTIVE
+        return self.device.state ^ OnoffState.active
 
     def activate(self):
         self.device.deactivate()
 
     def activate(self):
         self.device.deactivate()
@@ -73,50 +72,105 @@ class InvertedDevice(OnoffDevice):
         self.device.close()
 
 class ThrottledDevice(OnoffDevice):
         self.device.close()
 
 class ThrottledDevice(OnoffDevice):
-    """A device that delays deactivation by a fixed amount of time. This limits
-    the rate of state transitions to two per delay."""
-    def __init__(self, device, delay):
+    """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 device: OnoffDevice
-        @type delay: int or float
-        @param delay: deactivation delay in seconds
+        @type ondelay: int or float
+        @param ondelay: delay the report of OnoffState.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
         """
         OnoffDevice.__init__(self)
         self.device = device
-        self.delay = delay
-        self.desired_state = 0
-        self.stopping = None
+        self.ondelay = ondelay
+        self.offdelay = offdelay
+        self.desired_state = OnoffState.inactive
+        self.transition = None
         self.device.notify.add(self.changestate)
 
     @property
     def state(self):
         self.device.notify.add(self.changestate)
 
     @property
     def state(self):
-        return self.device.state | (ST_TRANSITION if self.stopping else 0)
+        if self.transition is None:
+            return self.device.state
+        return self.desired_state | OnoffState.transition
+
+    def _schedule_transition(self, delay, func):
+        assert self.transition is None
+        self.transition = ScheduledFunction(delay, func)
+
+    def _cancel_transition(self):
+        assert self.transition is not None
+        self.transition.cancel()
+        self.transition = None
+
+    def changestate(self, state):
+        if state != OnoffState.active:
+            OnoffDevice.changestate(self, state)
+        else:
+            if self.desired_state == OnoffState.inactive:
+                logger.warning("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 == OnoffState.active
+        assert self.transition is not None
+        self.transition = None
+        logger.debug("delivering activation signal")
+        OnoffDevice.changestate(self, OnoffState.active)
 
     def activate(self):
 
     def activate(self):
-        self.desired_state = ST_ACTIVE
-        if self.stopping is not None:
+        if self.desired_state == OnoffState.inactive and \
+                self.transition is not None:
             logger.debug("cancelling pending deactivation")
             logger.debug("cancelling pending deactivation")
-            ret = GObject.source_remove(self.stopping)
-            assert ret
-            self.stopping = None
+            self._cancel_transition()
+            curstate = self.device.state
+            if curstate == OnoffState.active:
+                self.desired_state = OnoffState.active
+                OnoffDevice.changestate(self, OnoffState.active)
+                return
+            logger.warning("device should be active during delayed " +
+                           "deactivation, but is in state %s", curstate.name)
+        self.desired_state = OnoffState.active
         self.device.activate()
         self.device.activate()
+        self.changestate(self.device.state)
 
 
-    def do_stop(self):
-        assert self.desired_state == 0
-        assert self.stopping is not None
-        self.stopping = None
+    def _do_stop(self):
+        assert self.desired_state == OnoffState.inactive
+        assert self.transition is not None
+        self.transition = None
         logger.debug("actually deactivating")
         self.device.deactivate()
 
     def deactivate(self):
         logger.debug("actually deactivating")
         self.device.deactivate()
 
     def deactivate(self):
-        self.desired_state = 0
-        if self.stopping is None:
-            logger.debug("scheduling actual deactivate in %.1fs", self.delay)
-            self.stopping = GObject.timeout_add(1000 * self.delay, self.do_stop)
+        if self.desired_state == OnoffState.active and \
+                self.transition is not None:
+            logger.debug("cancelling pending activation report")
+            self._cancel_transition()
+        self.desired_state = OnoffState.inactive
+        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):
             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 == OnoffState.inactive:
+                logger.info("invoking pending deactivate early during close")
+                self.device.deactivate()
         self.device.notify.remove(self.changestate)
         self.device.close()
         self.device.notify.remove(self.changestate)
         self.device.close()