X-Git-Url: https://git.linta.de/?p=~helmut%2Fonoff.git;a=blobdiff_plain;f=onoff%2Fcommon.py;h=5d9565cd553c144a2815f171a811389c020bd343;hp=e8fde4ad3a7b2838749e3f806ddd8b2dc6640832;hb=873cb2015c559466d35bda40f0fba8abbb4c8cb5;hpb=bba546949e4aa6bae3a20abe960834ab4cadba4d diff --git a/onoff/common.py b/onoff/common.py index e8fde4a..5d9565c 100644 --- a/onoff/common.py +++ b/onoff/common.py @@ -1,10 +1,176 @@ -""" -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. -""" - -ST_ACTIVE = 1 -ST_TRANSITION = 2 +import enum +import logging + +from .gobject import ScheduledFunction + +logger = logging.getLogger("onoff.common") + + +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 + 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 ^ OnoffState.active) + + @property + def state(self): + return self.device.state ^ OnoffState.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 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 + self.ondelay = ondelay + self.offdelay = offdelay + self.desired_state = OnoffState.inactive + 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 | 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): + if self.desired_state == OnoffState.inactive and \ + self.transition is not None: + logger.debug("cancelling pending deactivation") + 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.changestate(self.device.state) + + 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): + 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): + 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()