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