ThrottledDevice: speed up quick reactivation
[~helmut/onoff.git] / onoff / dbusutils.py
1 import argparse
2 import logging
3 import socket
4 import xml.parsers.expat
5
6 import dbus
7 import dbus.service
8 from gi.repository import GObject
9
10 logger = logging.getLogger("onoff.dbusutils")
11
12 object_prefix = "/de/subdivi/onoff0"
13 default_busname = "de.subdivi.onoff0"
14
15 dbus_options = argparse.ArgumentParser(add_help=False)
16 dbus_options.add_argument("--bus", default="session",
17                           choices=("system", "session"),
18                           help="which bus to use (default: %(default)s)")
19 dbus_options.add_argument("--busname", type=str, default=default_busname,
20                           help="which busname (i.e. client) to use " +
21                                "(default: %(default)s)")
22 dbus_options.add_argument("--device", type=str,
23                           help="which device to control")
24
25 def get_dbus(namespace):
26     """
27     @param namespace: a namespace returned from a dbus_options argument parser
28     @rtype: dbus.Bus
29     @returns: the requested bus
30     """
31     if namespace.bus == "session":
32         return dbus.SessionBus()
33     elif namespace.bus == "system":
34         return dbus.SystemBus()
35     else:
36         raise AssertionError("namespace.bus %r is neither session nor system",
37                              namespace.bus)
38
39 def get_dbus_proxy(namespace):
40     """
41     @param namespace: a namespace returned from a dbus_options argument parser
42     @returns: a dbus object proxy
43     """
44     bus = get_dbus(namespace)
45     if not namespace.device:
46         raise ValueError("no --device given")
47     objname = "%s/%s" % (object_prefix, namespace.device)
48     return bus.get_object(namespace.busname, objname)
49
50 def socketpair():
51     """Create a socket pair where the latter end is already wrapped for
52     transmission over dbus.
53
54     @rtype: (socket, dbus.types.UnixFd)
55     """
56     s1, s2 = socket.socketpair()
57     s3 = dbus.types.UnixFd(s2)
58     s2.close()
59     return s1, s3
60
61 def list_objects(bus, busname, path=None):
62     """List objects on the given bus and busname starting with path. Only the
63     trailing components after the slash are returned.
64
65     @type bus: dbus.Bus
66     @type busname: str
67     @type path: None or str
68     @param path: prefix for searching objects. Defaults to
69         dbusutils.object_prefix.
70     @rtype: [str]
71     @returns: the trailing components of the objects found
72     """
73     if path is None:
74         path = object_prefix
75     xmlstring = bus.get_object(busname, path).Introspect()
76     parser = xml.parsers.expat.ParserCreate()
77     nodes = []
78     def start_element(name, attrs):
79         if name != "node":
80             return
81         try:
82             value = attrs["name"]
83         except KeyError:
84             return
85         nodes.append(value)
86     parser.StartElementHandler = start_element
87     parser.Parse(xmlstring)
88     return nodes
89
90 class OnoffControl(dbus.service.Object):
91     domain = default_busname
92     path = object_prefix
93
94     def __init__(self, bus, name, device):
95         """
96         @type bus: dbus.Bus
97         @type name: str
98         @type device: OnoffDevice
99         """
100         busname = dbus.service.BusName(self.domain, bus=bus)
101         dbus.service.Object.__init__(self, busname, "%s/%s" % (self.path, name))
102         self.device = device
103         self.usecount = 0
104         device.notify.add(self.changestate)
105
106     @dbus.service.signal(domain, signature="q")
107     def changestate(self, state):
108         logger.debug("emitting state %d", state)
109
110     @dbus.service.method(domain, out_signature="q")
111     def state(self):
112         return self.device.state
113
114     @dbus.service.method(domain, in_signature="q", out_signature="q")
115     def activatetime(self, duration):
116         """Activate the device for a given number of seconds."""
117         logger.info("activatetime %d", duration)
118         GObject.timeout_add(duration * 1000, self.unuse)
119         return self.use()
120
121     @dbus.service.method(domain, in_signature="", out_signature="qh")
122     def activatefd(self):
123         """Activate a device until the returned file descriptor is closed."""
124         logger.info("activatefd")
125         notifyfd, retfd = socketpair()
126         def callback(fd, _):
127             logger.info("fd %d completed", fd.fileno())
128             fd.close()
129             self.unuse()
130             return False
131         GObject.io_add_watch(notifyfd, GObject.IO_HUP|GObject.IO_ERR, callback)
132         return (self.use(), retfd)
133
134     def use(self):
135         self.usecount += 1
136         if self.usecount <= 1:
137             self.device.activate()
138         return self.device.state
139
140     def unuse(self):
141         self.usecount -= 1
142         if not self.usecount:
143             self.device.deactivate()
144         else:
145             logger.debug("%d users left", self.usecount)
146         return False