1# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""
6Python implementation of the standard interfaces:
7  - org.freedesktop.DBus.Properties
8  - org.freedesktop.DBus.Introspectable (TODO(armansito): May not be necessary)
9  - org.freedesktop.DBus.ObjectManager
10
11"""
12
13import dbus
14import dbus.service
15import dbus.types
16import logging
17
18import pm_errors
19import utils
20
21from autotest_lib.client.cros.cellular import mm1_constants
22
23class MMPropertyError(pm_errors.MMError):
24    """
25    MMPropertyError is raised by DBusProperties methods
26    to indicate that a value for the given interface or
27    property could not be found.
28
29    """
30
31    UNKNOWN_PROPERTY = 0
32    UNKNOWN_INTERFACE = 1
33
34    def __init__(self, errno, *args, **kwargs):
35        super(MMPropertyError, self).__init__(errno, args, kwargs)
36
37
38    def _Setup(self):
39        self._error_name_base = mm1_constants.I_MODEM_MANAGER
40        self._error_name_map = {
41            self.UNKNOWN_PROPERTY : '.UnknownProperty',
42            self.UNKNOWN_INTERFACE : '.UnknownInterface'
43        }
44
45
46class DBusProperties(dbus.service.Object):
47    """
48    == org.freedesktop.DBus.Properties ==
49
50    This serves as the abstract base class for all objects that expose
51    properties. Each instance holds a mapping from DBus interface names to
52    property-value mappings, which are provided by the subclasses.
53
54    """
55
56    def __init__(self, path, bus=None, config=None):
57        """
58        @param bus: The pydbus bus object.
59        @param path: The DBus object path of this object.
60        @param config: This is an optional dictionary that can be used to
61                initialize the property dictionary with values other than the
62                ones provided by |_InitializeProperties|. The dictionary has to
63                contain a mapping from DBus interfaces to property-value pairs,
64                and all contained keys must have been initialized during
65                |_InitializeProperties|, i.e. if config contains any keys that
66                have not been already set in the internal property dictionary,
67                an error will be raised (see DBusProperties.Set).
68
69        """
70        if not path:
71            raise TypeError(('A value for "path" has to be provided that is '
72                'not "None".'))
73        if bus:
74          dbus.service.Object.__init__(self, bus, path)
75        else:
76          dbus.service.Object.__init__(self, None, None)
77        self.path = path
78        self.bus = bus
79        self._properties = self._InitializeProperties()
80
81        if config:
82            for key, props in config:
83                for prop, val in props:
84                    self.Set(key, prop, val)
85
86
87    @property
88    def properties(self):
89        """
90        @returns: The property dictionary.
91
92        """
93        return self._properties
94
95
96    def SetBus(self, bus):
97        """
98        Sets the pydbus bus object that this instance of DBusProperties should
99        be exposed on. Call this method only if |bus| is not already set.
100
101        @param bus: The pydbus bus object to assign.
102
103        """
104        self.bus = bus
105        self.add_to_connection(bus, self.path)
106
107
108    def SetPath(self, path):
109        """
110        Exposes this object on a new DBus path. This method fails with an
111        Exception by default, since exposing an object on multiple paths is
112        disallowed by default.
113
114        Subclasses can change this behavior by setting the
115        SUPPORTS_MULTIPLE_OBJECT_PATHS class variable to True.
116
117        @param path: The new path to assign to this object.
118
119        """
120        self.path = path
121        self.add_to_connection(self.bus, path)
122
123
124    def SetUInt32(self, interface_name, property_name, value):
125        """
126        Sets the given uint32 value matching the given property and interface.
127        Wraps the given value inside a dbus.types.UInt32.
128
129        @param interface_name: The DBus interface name.
130        @param property_name: The property name.
131        @param value: Value to set.
132        @raises: MMPropertyError, if the given |interface_name| or
133                |property_name| is not exposed by this object.
134        Emits:
135            PropertiesChanged
136
137        """
138        self.Set(interface_name, property_name, dbus.types.UInt32(value))
139
140
141    def SetInt32(self, interface_name, property_name, value):
142        """
143        Sets the given int32 value matching the given property and interface.
144        Wraps the given value inside a dbus.types.Int32.
145
146        @param interface_name: The DBus interface name.
147        @param property_name: The property name.
148        @param value: Value to set.
149        @raises: MMPropertyError, if the given |interface_name| or
150                |property_name| is not exposed by this object.
151        Emits:
152            PropertiesChanged
153
154        """
155        self.Set(interface_name, property_name, dbus.types.Int32(value))
156
157
158    @utils.log_dbus_method()
159    @dbus.service.method(mm1_constants.I_PROPERTIES, in_signature='ss',
160                         out_signature='v')
161    def Get(self, interface_name, property_name):
162        """
163        Returns the value matching the given property and interface.
164
165        @param interface_name: The DBus interface name.
166        @param property_name: The property name.
167        @returns: The value matching the given property and interface.
168        @raises: MMPropertyError, if the given |interface_name| or
169                |property_name| is not exposed by this object.
170
171        """
172        logging.info(
173            '%s: Get(%s, %s)',
174            self.path,
175            interface_name,
176            property_name)
177        val = self.GetAll(interface_name).get(property_name, None)
178        if val is None:
179            message = ("Property '%s' not implemented for interface '%s'." %
180                (property_name, interface_name))
181            logging.info(message)
182            raise MMPropertyError(
183                MMPropertyError.UNKNOWN_PROPERTY, message)
184        return val
185
186
187    @utils.log_dbus_method()
188    @dbus.service.method(mm1_constants.I_PROPERTIES, in_signature='ssv')
189    def Set(self, interface_name, property_name, value):
190        """
191        Sets the value matching the given property and interface.
192
193        @param interface_name: The DBus interface name.
194        @param property_name: The property name.
195        @param value: The value to set.
196        @raises: MMPropertyError, if the given |interface_name| or
197                |property_name| is not exposed by this object.
198        Emits:
199            PropertiesChanged
200
201        """
202        logging.info(
203            '%s: Set(%s, %s)',
204            self.path,
205            interface_name,
206            property_name)
207        props = self.GetAll(interface_name)
208        if property_name not in props:
209            raise MMPropertyError(
210                MMPropertyError.UNKNOWN_PROPERTY,
211                ("Property '%s' not implemented for "
212                "interface '%s'.") %
213                (property_name, interface_name))
214        if props[property_name] == value:
215            logging.info("Property '%s' already has value '%s'. Ignoring.",
216                         property_name,
217                         value)
218            return
219        props[property_name] = value
220        changed = { property_name : value }
221        inv = self._InvalidatedPropertiesForChangedValues(changed)
222        self.PropertiesChanged(interface_name, changed, inv)
223
224
225    @utils.log_dbus_method()
226    @dbus.service.method(mm1_constants.I_PROPERTIES,
227                         in_signature='s', out_signature='a{sv}')
228    def GetAll(self, interface_name):
229        """
230        Returns all property-value pairs that match the given interface.
231
232        @param interface_name: The DBus interface name.
233        @returns: A dictionary, containing the properties of the given DBus
234                interface and their values.
235        @raises: MMPropertyError, if the given |interface_name| or
236                |property_name| is not exposed by this object.
237
238        """
239        logging.info(
240            '%s: GetAll(%s)',
241            self.path,
242            interface_name)
243        props = self._properties.get(interface_name, None)
244        if props is None:
245            raise MMPropertyError(
246                MMPropertyError.UNKNOWN_INTERFACE,
247                "Object does not implement interface '%s'." %
248                interface_name)
249        return props
250
251
252    @dbus.service.signal(mm1_constants.I_PROPERTIES, signature='sa{sv}as')
253    def PropertiesChanged(
254            self,
255            interface_name,
256            changed_properties,
257            invalidated_properties):
258        """
259        This signal is emitted by Set, when the value of a property is changed.
260
261        @param interface_name: The interface the changed properties belong to.
262        @param changed_properties: Dictionary containing the changed properties
263                                   and their new values.
264        @param invalidated_properties: List of properties that were invalidated
265                                       when properties changed.
266
267        """
268        logging.info(('Properties Changed on interface: %s Changed Properties:'
269            ' %s InvalidatedProperties: %s.', interface_name,
270            str(changed_properties), str(invalidated_properties)))
271
272
273    def SetAll(self, interface, properties):
274        """
275        Sets the entire property dictionary for the given interface.
276
277        @param interface: String specifying the DBus interface.
278        @param properties: Dictionary containing the properties to set.
279        Emits:
280            PropertiesChanged
281
282        """
283        old_props = self._properties.get(interface, None)
284        if old_props:
285            invalidated = old_props.keys()
286        else:
287            invalidated = []
288        self._properties[interface] = properties
289        self.PropertiesChanged(interface, properties, invalidated)
290
291
292    def GetInterfacesAndProperties(self):
293        """
294        Returns all DBus properties of this object.
295
296        @returns: The complete property dictionary. The returned dict is a tree,
297                where the keys are DBus interfaces and the values are
298                dictionaries that map properties to values.
299        """
300        return self._properties
301
302
303    def _InvalidatedPropertiesForChangedValues(self, changed):
304        """
305        Called by Set, returns the list of property names that should become
306        invalidated given the properties and their new values contained in
307        changed. Subclasses can override this method; the default implementation
308        returns an empty list.
309
310        """
311        return []
312
313
314    def _InitializeProperties(self):
315        """
316        Called at instantiation. Subclasses have to override this method and
317        return a dictionary containing mappings from implemented interfaces to
318        dictionaries of property-value mappings.
319
320        """
321        raise NotImplementedError()
322
323
324class DBusObjectManager(dbus.service.Object):
325    """
326    == org.freedesktop.DBus.ObjectManager ==
327
328    This interface, included in rev. 0.17 of the DBus specification, allows a
329    generic way to control the addition and removal of Modem objects, as well
330    as the addition and removal of interfaces in the given objects.
331
332    """
333
334    def __init__(self, bus, path):
335        dbus.service.Object.__init__(self, bus, path)
336        self.devices = []
337        self.bus = bus
338        self.path = path
339
340
341    def Add(self, device):
342        """
343        Adds a device to the list of devices that are managed by this modem
344        manager.
345
346        @param device: Device to add.
347        Emits:
348            InterfacesAdded
349
350        """
351        self.devices.append(device)
352        device.manager = self
353        self.InterfacesAdded(device.path, device.GetInterfacesAndProperties())
354
355
356    def Remove(self, device):
357        """
358        Removes a device from the list of devices that are managed by this
359        modem manager.
360
361        @param device: Device to remove.
362        Emits:
363            InterfacesRemoved
364
365        """
366        if device in self.devices:
367            self.devices.remove(device)
368        interfaces = device.GetInterfacesAndProperties().keys()
369        self.InterfacesRemoved(device.path, interfaces)
370        device.remove_from_connection()
371
372
373    @utils.log_dbus_method()
374    @dbus.service.method(mm1_constants.I_OBJECT_MANAGER,
375                         out_signature='a{oa{sa{sv}}}')
376    def GetManagedObjects(self):
377        """
378        @returns: A dictionary containing all objects and their properties. The
379                keys to the dictionary are object paths which are mapped to
380                dictionaries containing mappings from DBus interface names to
381                property-value pairs.
382
383        """
384        results = {}
385        for device in self.devices:
386            results[dbus.types.ObjectPath(device.path)] = (
387                    device.GetInterfacesAndProperties())
388        logging.info('%s: GetManagedObjects: %s', self.path,
389                     ', '.join(results.keys()))
390        return results
391
392
393    @dbus.service.signal(mm1_constants.I_OBJECT_MANAGER,
394                         signature='oa{sa{sv}}')
395    def InterfacesAdded(self, object_path, interfaces_and_properties):
396        """
397        The InterfacesAdded signal is emitted when either a new object is added
398        or when an existing object gains one or more interfaces.
399
400        @param object_path: Path of the added object.
401        @param interfaces_and_properties: The complete property dictionary that
402                belongs to the recently added object.
403
404        """
405        logging.info((self.path + ': InterfacesAdded(' + object_path +
406                     ', ' + str(interfaces_and_properties)) + ')')
407
408
409    @dbus.service.signal(mm1_constants.I_OBJECT_MANAGER, signature='oas')
410    def InterfacesRemoved(self, object_path, interfaces):
411        """
412        The InterfacesRemoved signal is emitted whenever an object is removed
413        or it loses one or more interfaces.
414
415        @param object_path: Path of the remove object.
416        @param interfaces_and_properties: The complete property dictionary that
417                belongs to the recently removed object.
418
419        """
420        logging.info((self.path + ': InterfacesRemoved(' + object_path +
421                     ', ' + str(interfaces) + ')'))
422