#!/usr/bin/env python # -*- coding: utf-8 -*- import dbus.service import logging import traceback import os import weakref from ve_utils import wrap_dbus_value, unwrap_dbus_value # vedbus contains three classes: # VeDbusItemImport -> use this to read data from the dbus, ie import # VeDbusItemExport -> use this to export data to the dbus (one value) # VeDbusService -> use that to create a service and export several values to the dbus # Code for VeDbusItemImport is copied from busitem.py and thereafter modified. # All projects that used busitem.py need to migrate to this package. And some # projects used to define there own equivalent of VeDbusItemExport. Better to # use VeDbusItemExport, or even better the VeDbusService class that does it all for you. # TODOS # 1 check for datatypes, it works now, but not sure if all is compliant with # com.victronenergy.BusItem interface definition. See also the files in # tests_and_examples. And see 'if type(v) == dbus.Byte:' on line 102. Perhaps # something similar should also be done in VeDbusBusItemExport? # 2 Shouldn't VeDbusBusItemExport inherit dbus.service.Object? # 7 Make hard rules for services exporting data to the D-Bus, in order to make tracking # changes possible. Does everybody first invalidate its data before leaving the bus? # And what about before taking one object away from the bus, instead of taking the # whole service offline? # They should! And after taking one value away, do we need to know that someone left # the bus? Or we just keep that value in invalidated for ever? Result is that we can't # see the difference anymore between an invalidated value and a value that was first on # the bus and later not anymore. See comments above VeDbusItemImport as well. # 9 there are probably more todos in the code below. # Some thoughts with regards to the data types: # # Text from: http://dbus.freedesktop.org/doc/dbus-python/doc/tutorial.html#data-types # --- # Variants are represented by setting the variant_level keyword argument in the # constructor of any D-Bus data type to a value greater than 0 (variant_level 1 # means a variant containing some other data type, variant_level 2 means a variant # containing a variant containing some other data type, and so on). If a non-variant # is passed as an argument but introspection indicates that a variant is expected, # it'll automatically be wrapped in a variant. # --- # # Also the different dbus datatypes, such as dbus.Int32, and dbus.UInt32 are a subclass # of Python int. dbus.String is a subclass of Python standard class unicode, etcetera # # So all together that explains why we don't need to explicitly convert back and forth # between the dbus datatypes and the standard python datatypes. Note that all datatypes # in python are objects. Even an int is an object. # The signature of a variant is 'v'. # Export ourselves as a D-Bus service. class VeDbusService(object): def __init__(self, servicename, bus=None): # dict containing the VeDbusItemExport objects, with their path as the key. self._dbusobjects = {} self._dbusnodes = {} # dict containing the onchange callbacks, for each object. Object path is the key self._onchangecallbacks = {} # Connect to session bus whenever present, else use the system bus self._dbusconn = bus or (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else dbus.SystemBus()) # make the dbus connection available to outside, could make this a true property instead, but ach.. self.dbusconn = self._dbusconn # Register ourselves on the dbus, trigger an error if already in use (do_not_queue) self._dbusname = dbus.service.BusName(servicename, self._dbusconn, do_not_queue=True) # Add the root item that will return all items as a tree self._dbusnodes['/'] = self._create_tree_export(self._dbusconn, '/', self._get_tree_dict) logging.info("registered ourselves on D-Bus as %s" % servicename) def _get_tree_dict(self, path, get_text=False): logging.debug("_get_tree_dict called for %s" % path) r = {} px = path if not px.endswith('/'): px += '/' for p, item in self._dbusobjects.items(): if p.startswith(px): v = item.GetText() if get_text else wrap_dbus_value(item.local_get_value()) r[p[len(px):]] = v logging.debug(r) return r # To force immediate deregistering of this dbus service and all its object paths, explicitly # call __del__(). def __del__(self): for node in self._dbusnodes.values(): node.__del__() self._dbusnodes.clear() for item in self._dbusobjects.values(): item.__del__() self._dbusobjects.clear() if self._dbusname: self._dbusname.__del__() # Forces call to self._bus.release_name(self._name), see source code self._dbusname = None # @param callbackonchange function that will be called when this value is changed. First parameter will # be the path of the object, second the new value. This callback should return # True to accept the change, False to reject it. def add_path(self, path, value, description="", writeable=False, onchangecallback=None, gettextcallback=None): if onchangecallback is not None: self._onchangecallbacks[path] = onchangecallback item = VeDbusItemExport( self._dbusconn, path, value, description, writeable, self._value_changed, gettextcallback, deletecallback=self._item_deleted) spl = path.split('/') for i in range(2, len(spl)): subPath = '/'.join(spl[:i]) if subPath not in self._dbusnodes and subPath not in self._dbusobjects: self._dbusnodes[subPath] = self._create_tree_export(self._dbusconn, subPath, self._get_tree_dict) self._dbusobjects[path] = item logging.debug('added %s with start value %s. Writeable is %s' % (path, value, writeable)) # Add the mandatory paths, as per victron dbus api doc def add_mandatory_paths(self, processname, processversion, connection, deviceinstance, productid, productname, firmwareversion, hardwareversion, connected): self.add_path('/Mgmt/ProcessName', processname) self.add_path('/Mgmt/ProcessVersion', processversion) self.add_path('/Mgmt/Connection', connection) # Create rest of the mandatory objects self.add_path('/DeviceInstance', deviceinstance) self.add_path('/ProductId', productid) self.add_path('/ProductName', productname) self.add_path('/FirmwareVersion', firmwareversion) self.add_path('/HardwareVersion', hardwareversion) self.add_path('/Connected', connected) def _create_tree_export(self, bus, objectPath, get_value_handler): return VeDbusTreeExport(bus, objectPath, get_value_handler) # Callback function that is called from the VeDbusItemExport objects when a value changes. This function # maps the change-request to the onchangecallback given to us for this specific path. def _value_changed(self, path, newvalue): if path not in self._onchangecallbacks: return True return self._onchangecallbacks[path](path, newvalue) def _item_deleted(self, path): self._dbusobjects.pop(path) for np in self._dbusnodes.keys(): if np != '/': for ip in self._dbusobjects: if ip.startswith(np + '/'): break else: self._dbusnodes[np].__del__() self._dbusnodes.pop(np) def __getitem__(self, path): return self._dbusobjects[path].local_get_value() def __setitem__(self, path, newvalue): self._dbusobjects[path].local_set_value(newvalue) def __delitem__(self, path): self._dbusobjects[path].__del__() # Invalidates and then removes the object path assert path not in self._dbusobjects def __contains__(self, path): return path in self._dbusobjects """ Importing basics: - If when we power up, the D-Bus service does not exist, or it does exist and the path does not yet exist, still subscribe to a signal: as soon as it comes online it will send a signal with its initial value, which VeDbusItemImport will receive and use to update local cache. And, when set, call the eventCallback. - If when we power up, save it - When using get_value, know that there is no difference between services (or object paths) that don't exist and paths that are invalid (= empty array, see above). Both will return None. In case you do really want to know ifa path exists or not, use the exists property. - When a D-Bus service leaves the D-Bus, it will first invalidate all its values, and send signals with that update, and only then leave the D-Bus. (or do we need to subscribe to the NameOwnerChanged- signal!?!) To be discussed and make sure. Not really urgent, since all existing code that uses this class already subscribes to the NameOwnerChanged signal, and subsequently removes instances of this class. Read when using this class: Note that when a service leaves that D-Bus without invalidating all its exported objects first, for example because it is killed, VeDbusItemImport doesn't have a clue. So when using VeDbusItemImport, make sure to also subscribe to the NamerOwnerChanged signal on bus-level. Or just use dbusmonitor, because that takes care of all of that for you. """ class VeDbusItemImport(object): ## Constructor # @param bus the bus-object (SESSION or SYSTEM). # @param serviceName the dbus-service-name (string), for example 'com.victronenergy.battery.ttyO1' # @param path the object-path, for example '/Dc/V' # @param eventCallback function that you want to be called on a value change # @param createSignal only set this to False if you use this function to one time read a value. When # leaving it to True, make sure to also subscribe to the NameOwnerChanged signal # elsewhere. See also note some 15 lines up. def __init__(self, bus, serviceName, path, eventCallback=None, createsignal=True): # TODO: is it necessary to store _serviceName and _path? Isn't it # stored in the bus_getobjectsomewhere? self._serviceName = serviceName self._path = path self._match = None # TODO: _proxy is being used in settingsdevice.py, make a getter for that self._proxy = bus.get_object(serviceName, path, introspect=False) self.eventCallback = eventCallback assert eventCallback is None or createsignal == True if createsignal: self._match = self._proxy.connect_to_signal( "PropertiesChanged", weak_functor(self._properties_changed_handler)) # store the current value in _cachedvalue. When it doesn't exists set _cachedvalue to # None, same as when a value is invalid self._cachedvalue = None try: v = self._proxy.GetValue() except dbus.exceptions.DBusException: pass else: self._cachedvalue = unwrap_dbus_value(v) def __del__(self): if self._match != None: self._match.remove() self._match = None self._proxy = None def _refreshcachedvalue(self): self._cachedvalue = unwrap_dbus_value(self._proxy.GetValue()) ## Returns the path as a string, for example '/AC/L1/V' @property def path(self): return self._path ## Returns the dbus service name as a string, for example com.victronenergy.vebus.ttyO1 @property def serviceName(self): return self._serviceName ## Returns the value of the dbus-item. # the type will be a dbus variant, for example dbus.Int32(0, variant_level=1) # this is not a property to keep the name consistant with the com.victronenergy.busitem interface # returns None when the property is invalid def get_value(self): return self._cachedvalue ## Writes a new value to the dbus-item def set_value(self, newvalue): r = self._proxy.SetValue(wrap_dbus_value(newvalue)) # instead of just saving the value, go to the dbus and get it. So we have the right type etc. if r == 0: self._refreshcachedvalue() return r ## Returns the text representation of the value. # For example when the value is an enum/int GetText might return the string # belonging to that enum value. Another example, for a voltage, GetValue # would return a float, 12.0Volt, and GetText could return 12 VDC. # # Note that this depends on how the dbus-producer has implemented this. def get_text(self): return self._proxy.GetText() ## Returns true of object path exists, and false if it doesn't @property def exists(self): # TODO: do some real check instead of this crazy thing. r = False try: r = self._proxy.GetValue() r = True except dbus.exceptions.DBusException: pass return r ## callback for the trigger-event. # @param eventCallback the event-callback-function. @property def eventCallback(self): return self._eventCallback @eventCallback.setter def eventCallback(self, eventCallback): self._eventCallback = eventCallback ## Is called when the value of the imported bus-item changes. # Stores the new value in our local cache, and calls the eventCallback, if set. def _properties_changed_handler(self, changes): if "Value" in changes: changes['Value'] = unwrap_dbus_value(changes['Value']) self._cachedvalue = changes['Value'] if self._eventCallback: # The reason behind this try/except is to prevent errors silently ending up the an error # handler in the dbus code. try: self._eventCallback(self._serviceName, self._path, changes) except: traceback.print_exc() os._exit(1) # sys.exit() is not used, since that also throws an exception class VeDbusTreeExport(dbus.service.Object): def __init__(self, bus, objectPath, get_value_handler): dbus.service.Object.__init__(self, bus, objectPath) self._get_value_handler = get_value_handler logging.debug("VeDbusTreeExport %s has been created" % objectPath) def __del__(self): # self._get_path() will raise an exception when retrieved after the call to .remove_from_connection, # so we need a copy. path = self._get_path() if path is None: return self.remove_from_connection() logging.debug("VeDbusTreeExport %s has been removed" % path) def _get_path(self): if len(self._locations) == 0: return None return self._locations[0][1] @dbus.service.method('com.victronenergy.BusItem', out_signature='v') def GetValue(self): value = self._get_value_handler(self._get_path()) return dbus.Dictionary(value, signature=dbus.Signature('sv'), variant_level=1) @dbus.service.method('com.victronenergy.BusItem', out_signature='v') def GetText(self): return self._get_value_handler(self._get_path(), True) def local_get_value(self): return self._get_value_handler(self.path) class VeDbusItemExport(dbus.service.Object): ## Constructor of VeDbusItemExport # # Use this object to export (publish), values on the dbus # Creates the dbus-object under the given dbus-service-name. # @param bus The dbus object. # @param objectPath The dbus-object-path. # @param value Value to initialize ourselves with, defaults to None which means Invalid # @param description String containing a description. Can be called over the dbus with GetDescription() # @param writeable what would this do!? :). # @param callback Function that will be called when someone else changes the value of this VeBusItem # over the dbus. First parameter passed to callback will be our path, second the new # value. This callback should return True to accept the change, False to reject it. def __init__(self, bus, objectPath, value=None, description=None, writeable=False, onchangecallback=None, gettextcallback=None, deletecallback=None): dbus.service.Object.__init__(self, bus, objectPath) self._onchangecallback = onchangecallback self._gettextcallback = gettextcallback self._value = value self._description = description self._writeable = writeable self._deletecallback = deletecallback # To force immediate deregistering of this dbus object, explicitly call __del__(). def __del__(self): # self._get_path() will raise an exception when retrieved after the # call to .remove_from_connection, so we need a copy. path = self._get_path() if path == None: return if self._deletecallback is not None: self._deletecallback(path) self.local_set_value(None) self.remove_from_connection() logging.debug("VeDbusItemExport %s has been removed" % path) def _get_path(self): if len(self._locations) == 0: return None return self._locations[0][1] ## Sets the value. And in case the value is different from what it was, a signal # will be emitted to the dbus. This function is to be used in the python code that # is using this class to export values to the dbus. # set value to None to indicate that it is Invalid def local_set_value(self, newvalue): if self._value == newvalue: return self._value = newvalue changes = {} changes['Value'] = wrap_dbus_value(newvalue) changes['Text'] = self.GetText() self.PropertiesChanged(changes) def local_get_value(self): return self._value # ==== ALL FUNCTIONS BELOW THIS LINE WILL BE CALLED BY OTHER PROCESSES OVER THE DBUS ==== ## Dbus exported method SetValue # Function is called over the D-Bus by other process. It will first check (via callback) if new # value is accepted. And it is, stores it and emits a changed-signal. # @param value The new value. # @return completion-code When successful a 0 is return, and when not a -1 is returned. @dbus.service.method('com.victronenergy.BusItem', in_signature='v', out_signature='i') def SetValue(self, newvalue): if not self._writeable: return 1 # NOT OK newvalue = unwrap_dbus_value(newvalue) if newvalue == self._value: return 0 # OK # call the callback given to us, and check if new value is OK. if (self._onchangecallback is None or (self._onchangecallback is not None and self._onchangecallback(self.__dbus_object_path__, newvalue))): self.local_set_value(newvalue) return 0 # OK return 2 # NOT OK ## Dbus exported method GetDescription # # Returns the a description. # @param language A language code (e.g. ISO 639-1 en-US). # @param length Lenght of the language string. # @return description @dbus.service.method('com.victronenergy.BusItem', in_signature='si', out_signature='s') def GetDescription(self, language, length): return self._description if self._description is not None else 'No description given' ## Dbus exported method GetValue # Returns the value. # @return the value when valid, and otherwise an empty array @dbus.service.method('com.victronenergy.BusItem', out_signature='v') def GetValue(self): return wrap_dbus_value(self._value) ## Dbus exported method GetText # Returns the value as string of the dbus-object-path. # @return text A text-value. '---' when local value is invalid @dbus.service.method('com.victronenergy.BusItem', out_signature='s') def GetText(self): if self._value is None: return '---' # Default conversion from dbus.Byte will get you a character (so 'T' instead of '84'), so we # have to convert to int first. Note that if a dbus.Byte turns up here, it must have come from # the application itself, as all data from the D-Bus should have been unwrapped by now. if self._gettextcallback is None and type(self._value) == dbus.Byte: return str(int(self._value)) if self._gettextcallback is None and self.__dbus_object_path__ == '/ProductId': return "0x%X" % self._value if self._gettextcallback is None: return str(self._value) return self._gettextcallback(self.__dbus_object_path__, self._value) ## The signal that indicates that the value has changed. # Other processes connected to this BusItem object will have subscribed to the # event when they want to track our state. @dbus.service.signal('com.victronenergy.BusItem', signature='a{sv}') def PropertiesChanged(self, changes): pass ## This class behaves like a regular reference to a class method (eg. self.foo), but keeps a weak reference ## to the object which method is to be called. ## Use this object to break circular references. class weak_functor: def __init__(self, f): self._r = weakref.ref(f.__self__) self._f = weakref.ref(f.__func__) def __call__(self, *args, **kargs): r = self._r() f = self._f() if r == None or f == None: return f(r, *args, **kargs)