diff --git a/PackageManager.py b/PackageManager.py index 03d30f8..7e79aaf 100755 --- a/PackageManager.py +++ b/PackageManager.py @@ -368,20 +368,13 @@ import time import re import glob +import queue +from gi.repository import GLib +# add the path to our own packages for import +sys.path.insert(1, "/data/SetupHelper/velib_python") +from vedbus import VeDbusService +from settingsdevice import SettingsDevice -# accommodate both Python 2 (prior to v2.80) and 3 -# note subprocess.run and subprocess.DEVNULL do not exist in python 2 -# so subprocess.Popen and subprocess.PIPE and subprocess.communicate () -# are used in all subprodess calls even if process output is not needed -# or if it is not necessary to wait for the command to finish - -PythonVersion = sys.version_info -if PythonVersion >= (3, 0): - import queue - from gi.repository import GLib -else: - import Queue as queue - import gobject as GLib global DownloadGitHub global InstallPackages @@ -483,42 +476,6 @@ def VersionToNumber (version): VenusVersionNumber = VersionToNumber (VenusVersion) file.close() -# add the path to our own packages for import -# use an established Victron service to maintain compatiblity -setupHelperVeLibPath = "/data/SetupHelper/velib_python" -veLibPath = "" -if os.path.exists ( setupHelperVeLibPath ): - for libVersion in os.listdir ( setupHelperVeLibPath ): - # use 'latest' for newest versions even if not specifically checked against this verison when created - if libVersion == "latest": - newestVersionNumber = VersionToNumber ( "v9999.9999.9999" ) - else: - newestVersionNumber = VersionToNumber ( libVersion ) - oldestVersionPath = os.path.join (setupHelperVeLibPath, libVersion, "oldestVersion" ) - if os.path.exists ( oldestVersionPath ): - try: - fd = open (oldestVersionPath, 'r') - oldestVersionNumber = VersionToNumber ( fd.readline().strip () ) - fd.close() - except: - oldestVersionNumber = 0 - else: - oldestVersionNumber = 0 - if VenusVersionNumber >= oldestVersionNumber and VenusVersionNumber <= newestVersionNumber: - veLibPath = os.path.join (setupHelperVeLibPath, libVersion) - break - -# no SetupHelper velib - use one in systemcalc -if veLibPath == "": - veLibPath = os.path.join('/opt/victronenergy/dbus-systemcalc-py', 'ext', 'velib_python') - -logging.warning ("using " + veLibPath + " for velib_python") -sys.path.insert(1, veLibPath) - -from vedbus import VeDbusService -from settingsdevice import SettingsDevice - - # PushAction # # some actions are pushed to one of three queues: @@ -1241,13 +1198,7 @@ def __init__(self): # check firmware version and delay dbus service registration for v3.40~38 and beyond global VenusVersionNumber global VersionToNumber - versionThreshold = VersionToNumber ("v3.40~28") - if VenusVersionNumber >= versionThreshold: - self.DbusService = VeDbusService ('com.victronenergy.packageManager', bus = dbus.SystemBus(), register=False) - delayedRegistration = True - else: - self.DbusService = VeDbusService ('com.victronenergy.packageManager', bus = dbus.SystemBus()) - delayedRegistration = False + self.DbusService = VeDbusService ('com.victronenergy.packageManager', bus = dbus.SystemBus(), register=False) self.DbusService.add_mandatory_paths ( processname = 'PackageManager', processversion = 1.0, connection = 'none', @@ -1289,8 +1240,8 @@ def __init__(self): self.DbusService.add_path ( '/PmStatus', "", writeable = True ) global Platform self.DbusService.add_path ( '/Platform', Platform ) - if delayedRegistration: - self.DbusService.register () + + self.DbusService.register () # RemoveDbusService @@ -4055,9 +4006,6 @@ def main(): # Have a mainloop, so we can send/receive asynchronous calls to and from dbus DBusGMainLoop(set_as_default=True) - global PythonVersion - if PythonVersion < (3, 0): - GLib.threads_init() # get platform global Platform diff --git a/ReadMe b/ReadMe index ca1a42f..2908ab6 100644 --- a/ReadMe +++ b/ReadMe @@ -22,6 +22,13 @@ The SetupHelper package provides: Restart or initialize PackageManager Restart the GUI +NOTE: Support for firmware prior to v3.10 has been dropped starting with SetupHelper v8.10 + if you are running older versions, change the branch/tag to preV3.10support + for any packages you wish to run on that firmware + + While this branch will remain active, there will be no features added to it + and only serious bug fixes will be applied. + SetupHelper v8 adds the ability for multiple packages to modify the same file Packages must be written to "patch" a file rather than "replace" it diff --git a/blindInstall/SetupHelperVersion b/blindInstall/SetupHelperVersion index 1416d7e..5eb381b 100644 --- a/blindInstall/SetupHelperVersion +++ b/blindInstall/SetupHelperVersion @@ -1 +1 @@ -v8.9 +v8.10 diff --git a/changes b/changes index 4c682ed..f5f631d 100644 --- a/changes +++ b/changes @@ -1,3 +1,7 @@ +v8.10: + moved velib_python in SetupHelper to a single version + dropping support for firmware earlier than v3.10 + v8.9: fixed: further changes for the remote GUI issue diff --git a/updatePackage b/updatePackage index b934c84..58d5baf 100755 --- a/updatePackage +++ b/updatePackage @@ -876,9 +876,15 @@ for package in $packageList; do oldReplacementVersion="-" oldReplacementIsFile=false oldReplacementIsUseOrig=false + blockIsUnused=true for (( i1 = start; i1 < $allFileSetsLength; i1++ )); do IFS=':' read version versionNumber <<< "${allFileSets[$i1]}" fileSet="$workingFiles/$version" + if [ -e "$fileSet/INCOMPATIBLE_VERSION" ] || [ -e "$fileSet/UNUSED_FILE_SET" ]; then + fileSetUnused=true + else + fileSetUnused=false + fi replacement="$fileSet/$baseName" stockFileSet="$stockFiles/$version" orig="$fileSet/$baseName.orig" @@ -939,6 +945,10 @@ for package in $packageList; do break fi + if ! $fileSetUnused ; then + blockIsUnused=false + fi + # save version of old replacement files for next loop if $replacementIsFile ; then if ! $oldReplacementIsFile || [ "$oldReplacementVersion" == "-" ]; then @@ -958,24 +968,16 @@ for package in $packageList; do # first compatible file set - move replacements here # unused file sets are permitted for the destination file set # but if a used file set is later found, it is preferred - if [ -e "$fileSet/INCOMPATIBLE_VERSION" ]; then - updateTo=false - elif (( to == -1 )); then - updateTo=true - elif ! [ -e "$fileSet/UNUSED_FILE_SET" ] && [ -e "$toFileSet/UNUSED_FILE_SET" ]; then - updateTo=true - else - updateTo=false - fi - if $updateTo ; then + if (( to == -1 )) && ! $fileSetUnused; then (( to = i1 )) toFileSet="$fileSet" toVersion=$( basename "$toFileSet" ) fi done # end locate block - if (( to == -1 )); then - logMessage "ERROR $package: $baseName no destination file set - can't relocate files" + if ! $blockIsUnused; then + logMessage "ERROR $package: $baseName no destination file set for block $startVersion $endVersion - can't relocate files" + fi else toReplacement="$toFileSet/$baseName" oldReplacement="$workingFiles/$oldReplacementVersion/$baseName" @@ -1099,29 +1101,40 @@ for package in $packageList; do break fi done - # LINKS_ONLY is not used for anything but helps identify file sets - # that don't contain real files + # LINKS_ONLY is not used for anything but helps identify file sets that don't contain real files if ! $replacementFilesExist ; then touch "$fileSet/LINKS_ONLY" fi + fileSetInUse=true # remove file sets for incompatible Venus OS versions if [ -e "$fileSet/INCOMPATIBLE_VERSION" ]; then - if [ ! -f "$fileSet/NEW_FILE_SET" ]; then - logMessage "WARNING $package: not compatible with Venus $version - file set removed" + fileSetInUse=false + if $replacementFilesExist ; then + logMessage "WARNING $package: $version not compatible with Venus $version but file set not empty" + else + if [ ! -f "$fileSet/NEW_FILE_SET" ]; then + logMessage "WARNING $package: not compatible with Venus $version - file set removed" + fi + rm -Rf "$fileSet" fi - rm -Rf "$fileSet" # remove empty unused file sets elif [ -f "$fileSet/UNUSED_FILE_SET" ]; then - # log removal of a previous file set if not created with this run - # if it was created with this run, delete it silently - if [ ! -f "$fileSet/NEW_FILE_SET" ]; then - logMessage "WARNING $package: $version - removing unused file set" + fileSetInUse=false + if $replacementFilesExist ; then + logMessage "WARNING $package: $version no longer used but file set not empty" + else + # log removal of a previous file set if not created with this run + # if it was created with this run, delete it silently + if [ ! -f "$fileSet/NEW_FILE_SET" ]; then + logMessage "WARNING $package: $version - removing unused file set" + fi + rm -Rf "$fileSet" fi - rm -Rf "$fileSet" + fi # do final checks on versioned files only # and for file sets that still exist - else + if [ -e "$fileSet" ] && $fileSetInUse; then if [ -e "$fileSet/NEW_FILE_SET" ]; then logMessage "$package: $version new file set" fi @@ -1172,7 +1185,6 @@ for package in $packageList; do logMessage "ERROR $package $baseName: no replacement for $version" fi - # validate sym link symLinkReplacement=false badLink=false @@ -1181,19 +1193,18 @@ for package in $packageList; do # resolive symlink then check to make sure that file is valid linkedFile=$( realpath "$replacement" 2> /dev/null ) if [ -z "$linkedFile" ]; then - badLink=true + logMessage "ERROR $package: $baseName $version no linked file $linkedFile" + touch "$replacement.BAD_LINK" else linkedFileSet=$( dirname "$linkedFile" ) if [ -z "$linkedFileSet" ]; then - badLink=true + logMessage "ERROR $package: $baseName $version no linked file set" + touch "$replacement.BAD_LINK" elif [ -f "$linkedFileSet/UNUSED_FILE_SET" ] || [ -f "$linkedFileSet/INCOMPATIBLE_VERSION" ]; then - badLink=true + logMessage "ERROR $package: $baseName $version links to unused file set $linkedFileSet" + touch "$replacement.BAD_LINK" fi fi - if $badLink ; then - logMessage "ERROR $package: $baseName $version bad link" - touch "$replacement.BAD_LINK" - fi fi # flag file set incomplete diff --git a/velib_python/latest/dbusmonitor.py b/velib_python/dbusmonitor.py similarity index 100% rename from velib_python/latest/dbusmonitor.py rename to velib_python/dbusmonitor.py diff --git a/velib_python/latest/oldestVersion b/velib_python/oldestVersion similarity index 100% rename from velib_python/latest/oldestVersion rename to velib_python/oldestVersion diff --git a/velib_python/latest/settingsdevice.py b/velib_python/settingsdevice.py similarity index 100% rename from velib_python/latest/settingsdevice.py rename to velib_python/settingsdevice.py diff --git a/velib_python/v2.73/dbusmonitor.py b/velib_python/v2.73/dbusmonitor.py deleted file mode 100644 index fc8785d..0000000 --- a/velib_python/v2.73/dbusmonitor.py +++ /dev/null @@ -1,535 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -## @package dbus_vrm -# This code takes care of the D-Bus interface (not all of below is implemented yet): -# - on startup it scans the dbus for services we know. For each known service found, it searches for -# objects/paths we know. Everything we find is stored in items{}, and an event is registered: if a -# value changes weĺl be notified and can pass that on to our owner. For example the vrmLogger. -# we know. -# - after startup, it continues to monitor the dbus: -# 1) when services are added we do the same check on that -# 2) when services are removed, we remove any items that we had that referred to that service -# 3) if an existing services adds paths we update ourselves as well: on init, we make a -# VeDbusItemImport for a non-, or not yet existing objectpaths as well1 -# -# Code is used by the vrmLogger, and also the pubsub code. Both are other modules in the dbus_vrm repo. - -from dbus.mainloop.glib import DBusGMainLoop -import gobject -from gobject import idle_add -import dbus -import dbus.service -import inspect -import logging -import argparse -import pprint -import traceback -import os -from collections import defaultdict -from functools import partial - -# our own packages -from vedbus import VeDbusItemExport, VeDbusItemImport -from ve_utils import exit_on_error, wrap_dbus_value, unwrap_dbus_value -notfound = object() # For lookups where None is a valid result - -logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) -class SystemBus(dbus.bus.BusConnection): - def __new__(cls): - return dbus.bus.BusConnection.__new__(cls, dbus.bus.BusConnection.TYPE_SYSTEM) - -class SessionBus(dbus.bus.BusConnection): - def __new__(cls): - return dbus.bus.BusConnection.__new__(cls, dbus.bus.BusConnection.TYPE_SESSION) - -class MonitoredValue(object): - def __init__(self, value, text, options): - super(MonitoredValue, self).__init__() - self.value = value - self.text = text - self.options = options - - # For legacy code, allow treating this as a tuple/list - def __iter__(self): - return iter((self.value, self.text, self.options)) - -class Service(object): - whentologoptions = ['configChange', 'onIntervalAlwaysAndOnEvent', - 'onIntervalOnlyWhenChanged', 'onIntervalAlways', 'never'] - def __init__(self, id, serviceName, deviceInstance): - super(Service, self).__init__() - self.id = id - self.name = serviceName - self.paths = {} - self._seen = set() - self.deviceInstance = deviceInstance - - self.configChange = [] - self.onIntervalAlwaysAndOnEvent = [] - self.onIntervalOnlyWhenChanged = [] - self.onIntervalAlways = [] - self.never = [] - - # For legacy code, attributes can still be accessed as if keys from a - # dictionary. - def __setitem__(self, key, value): - self.__dict__[key] = value - def __getitem__(self, key): - return self.__dict__[key] - - def set_seen(self, path): - self._seen.add(path) - - def seen(self, path): - return path in self._seen - - @property - def service_class(self): - return '.'.join(self.name.split('.')[:3]) - -class DbusMonitor(object): - ## Constructor - def __init__(self, dbusTree, valueChangedCallback=None, deviceAddedCallback=None, - deviceRemovedCallback=None, vebusDeviceInstance0=False): - # valueChangedCallback is the callback that we call when something has changed. - # def value_changed_on_dbus(dbusServiceName, dbusPath, options, changes, deviceInstance): - # in which changes is a tuple with GetText() and GetValue() - self.valueChangedCallback = valueChangedCallback - self.deviceAddedCallback = deviceAddedCallback - self.deviceRemovedCallback = deviceRemovedCallback - self.dbusTree = dbusTree - self.vebusDeviceInstance0 = vebusDeviceInstance0 - - # Lists all tracked services. Stores name, id, device instance, value per path, and whenToLog info - # indexed by service name (eg. com.victronenergy.settings). - self.servicesByName = {} - - # Same values as self.servicesByName, but indexed by service id (eg. :1.30) - self.servicesById = {} - - # Keep track of services by class to speed up calls to get_service_list - self.servicesByClass = defaultdict(list) - - # Keep track of any additional watches placed on items - self.serviceWatches = defaultdict(list) - - # For a PC, connect to the SessionBus - # For a CCGX, connect to the SystemBus - self.dbusConn = SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else SystemBus() - - # subscribe to NameOwnerChange for bus connect / disconnect events. - (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ \ - else dbus.SystemBus()).add_signal_receiver( - self.dbus_name_owner_changed, - signal_name='NameOwnerChanged') - - # Subscribe to PropertiesChanged for all services - self.dbusConn.add_signal_receiver(self.handler_value_changes, - dbus_interface='com.victronenergy.BusItem', - signal_name='PropertiesChanged', path_keyword='path', - sender_keyword='senderId') - - logger.info('===== Search on dbus for services that we will monitor starting... =====') - serviceNames = self.dbusConn.list_names() - for serviceName in serviceNames: - self.scan_dbus_service(serviceName) - - logger.info('===== Search on dbus for services that we will monitor finished =====') - - def dbus_name_owner_changed(self, name, oldowner, newowner): - if not name.startswith("com.victronenergy."): - return - - #decouple, and process in main loop - idle_add(exit_on_error, self._process_name_owner_changed, name, oldowner, newowner) - - def _process_name_owner_changed(self, name, oldowner, newowner): - if newowner != '': - # so we found some new service. Check if we can do something with it. - newdeviceadded = self.scan_dbus_service(name) - if newdeviceadded and self.deviceAddedCallback is not None: - self.deviceAddedCallback(name, self.get_device_instance(name)) - - elif name in self.servicesByName: - # it disappeared, we need to remove it. - logger.info("%s disappeared from the dbus. Removing it from our lists" % name) - service = self.servicesByName[name] - deviceInstance = service['deviceInstance'] - del self.servicesById[service.id] - del self.servicesByName[name] - for watch in self.serviceWatches[name]: - watch.remove() - del self.serviceWatches[name] - self.servicesByClass[service.service_class].remove(service) - if self.deviceRemovedCallback is not None: - self.deviceRemovedCallback(name, deviceInstance) - - def scan_dbus_service(self, serviceName): - try: - return self.scan_dbus_service_inner(serviceName) - except: - logger.error("Ignoring %s because of error while scanning:" % (serviceName)) - traceback.print_exc() - return False - - # Errors 'org.freedesktop.DBus.Error.ServiceUnknown' and - # 'org.freedesktop.DBus.Error.Disconnected' seem to happen when the service - # disappears while its being scanned. Which might happen, but is not really - # normal either, so letting them go into the logs. - - # Scans the given dbus service to see if it contains anything interesting for us. If it does, add - # it to our list of monitored D-Bus services. - def scan_dbus_service_inner(self, serviceName): - - # make it a normal string instead of dbus string - serviceName = str(serviceName) - - paths = self.dbusTree.get('.'.join(serviceName.split('.')[0:3]), None) - if paths is None: - logger.debug("Ignoring service %s, not in the tree" % serviceName) - return False - - logger.info("Found: %s, scanning and storing items" % serviceName) - serviceId = self.dbusConn.get_name_owner(serviceName) - - # we should never be notified to add a D-Bus service that we already have. If this assertion - # raises, check process_name_owner_changed, and D-Bus workings. - assert serviceName not in self.servicesByName - assert serviceId not in self.servicesById - - # for vebus.ttyO1, this is workaround, since VRM Portal expects the main vebus - # devices at instance 0. Not sure how to fix this yet. - if serviceName == 'com.victronenergy.vebus.ttyO1' and self.vebusDeviceInstance0: - di = 0 - elif serviceName == 'com.victronenergy.settings': - di = 0 - elif serviceName.startswith('com.victronenergy.vecan.'): - di = 0 - else: - try: - di = self.dbusConn.call_blocking(serviceName, - '/DeviceInstance', None, 'GetValue', '', []) - except dbus.exceptions.DBusException: - logger.info(" %s was skipped because it has no device instance" % serviceName) - return False # Skip it - else: - di = int(di) - - logger.info(" %s has device instance %s" % (serviceName, di)) - service = Service(serviceId, serviceName, di) - - # Let's try to fetch everything in one go - values = {} - texts = {} - try: - values.update(self.dbusConn.call_blocking(serviceName, '/', None, 'GetValue', '', [])) - texts.update(self.dbusConn.call_blocking(serviceName, '/', None, 'GetText', '', [])) - except: - pass - - for path, options in paths.iteritems(): - # path will be the D-Bus path: '/Ac/ActiveIn/L1/V' - # options will be a dictionary: {'code': 'V', 'whenToLog': 'onIntervalAlways'} - # check that the whenToLog setting is set to something we expect - assert options['whenToLog'] is None or options['whenToLog'] in Service.whentologoptions - - # Try to obtain the value we want from our bulk fetch. If we - # cannot find it there, do an individual query. - value = values.get(path[1:], notfound) - if value != notfound: - service.set_seen(path) - text = texts.get(path[1:], notfound) - if value is notfound or text is notfound: - try: - value = self.dbusConn.call_blocking(serviceName, path, None, 'GetValue', '', []) - service.set_seen(path) - text = self.dbusConn.call_blocking(serviceName, path, None, 'GetText', '', []) - except dbus.exceptions.DBusException as e: - if e.get_dbus_name() in ( - 'org.freedesktop.DBus.Error.ServiceUnknown', - 'org.freedesktop.DBus.Error.Disconnected'): - raise # This exception will be handled below - - # TODO org.freedesktop.DBus.Error.UnknownMethod really - # shouldn't happen but sometimes does. - logger.debug("%s %s does not exist (yet)" % (serviceName, path)) - value = None - text = None - - service.paths[path] = MonitoredValue(unwrap_dbus_value(value), unwrap_dbus_value(text), options) - - if options['whenToLog']: - service[options['whenToLog']].append(path) - - - logger.debug("Finished scanning and storing items for %s" % serviceName) - - # Adjust self at the end of the scan, so we don't have an incomplete set of - # data if an exception occurs during the scan. - self.servicesByName[serviceName] = service - self.servicesById[serviceId] = service - self.servicesByClass[service.service_class].append(service) - - return True - - def handler_value_changes(self, changes, path, senderId): - try: - service = self.servicesById[senderId] - a = service.paths[path] - except KeyError: - # Either senderId or path isn't there, which means - # it hasn't been scanned yet. - return - - # If this properyChange does not involve a value, our work is done. - if 'Value' not in changes: - return - - service.set_seen(path) - - # First update our store to the new value - changes['Value'] = unwrap_dbus_value(changes['Value']) - if a.value == changes['Value']: - return - - a.value = changes['Value'] - try: - a.text = changes['Text'] - except KeyError: - # Some services don't send Text with their PropertiesChanged events. - a.text = str(a.value) - - # And do the rest of the processing in on the mainloop - if self.valueChangedCallback is not None: - idle_add(exit_on_error, self._execute_value_changes, service.name, path, changes, a.options) - - def _execute_value_changes(self, serviceName, objectPath, changes, options): - # double check that the service still exists, as it might have - # disappeared between scheduling-for and executing this function. - if serviceName not in self.servicesByName: - return - - self.valueChangedCallback(serviceName, objectPath, - options, changes, self.get_device_instance(serviceName)) - - # Gets the value for a certain servicename and path - # The default_value is returned when: - # 1. When the service doesn't exist. - # 2. When the path asked for isn't being monitored. - # 3. When the path exists, but has dbus-invalid, ie an empty byte array. - # 4. When the path asked for is being monitored, but doesn't exist for that service. - def get_value(self, serviceName, objectPath, default_value=None): - service = self.servicesByName.get(serviceName, None) - if service is None: - return default_value - - value = service.paths.get(objectPath, None) - if value is None or value.value is None: - return default_value - - return value.value - - # returns if a dbus exists now, by doing a blocking dbus call. - # Typically seen will be sufficient and doesn't need access to the dbus. - def exists(self, serviceName, objectPath): - try: - self.dbusConn.call_blocking(serviceName, objectPath, None, 'GetValue', '', []) - return True - except dbus.exceptions.DBusException as e: - return False - - # Returns if there ever was a successful GetValue or valueChanged event. - # Unlike get_value this return True also if the actual value is invalid. - # - # Note: the path might no longer exists anymore, but that doesn't happen in - # practice. If a service really wants to reconfigure itself typically it should - # reconnect to the dbus which causes it to be rescanned and seen will be updated. - # If it is really needed to know if a path still exists, use exists. - def seen(self, serviceName, objectPath): - try: - return self.servicesByName[serviceName].seen(objectPath) - except KeyError: - return False - - # Sets the value for a certain servicename and path, returns the return value of the D-Bus SetValue - # method. If the underlying item does not exist (the service does not exist, or the objectPath was not - # registered) the function will return -1 - def set_value(self, serviceName, objectPath, value): - # Check if the D-Bus object referenced by serviceName and objectPath is registered. There is no - # necessity to do this, but it is in line with previous implementations which kept VeDbusItemImport - # objects for registers items only. - service = self.servicesByName.get(serviceName, None) - if service is None: - return -1 - if objectPath not in service.paths: - return -1 - # We do not catch D-Bus exceptions here, because the previous implementation did not do that either. - return self.dbusConn.call_blocking(serviceName, objectPath, - dbus_interface='com.victronenergy.BusItem', - method='SetValue', signature=None, - args=[wrap_dbus_value(value)]) - - # Similar to set_value, but operates asynchronously - def set_value_async(self, serviceName, objectPath, value, - reply_handler=None, error_handler=None): - service = self.servicesByName.get(serviceName, None) - if service is not None: - if objectPath in service.paths: - self.dbusConn.call_async(serviceName, objectPath, - dbus_interface='com.victronenergy.BusItem', - method='SetValue', signature=None, - args=[wrap_dbus_value(value)], - reply_handler=reply_handler, error_handler=error_handler) - return - - if error_handler is not None: - error_handler(TypeError('Service or path not found, ' - 'service=%s, path=%s' % (serviceName, objectPath))) - - # returns a dictionary, keys are the servicenames, value the instances - # optionally use the classfilter to get only a certain type of services, for - # example com.victronenergy.battery. - def get_service_list(self, classfilter=None): - if classfilter is None: - return { servicename: service.deviceInstance \ - for servicename, service in self.servicesByName.iteritems() } - - if classfilter not in self.servicesByClass: - return {} - - return { service.name: service.deviceInstance \ - for service in self.servicesByClass[classfilter] } - - def get_device_instance(self, serviceName): - return self.servicesByName[serviceName].deviceInstance - - # Parameter categoryfilter is to be a list, containing the categories you want (configChange, - # onIntervalAlways, etc). - # Returns a dictionary, keys are codes + instance, in VRM querystring format. For example vvt[0]. And - # values are the value. - def get_values(self, categoryfilter, converter=None): - - result = {} - - for serviceName in self.servicesByName: - result.update(self.get_values_for_service(categoryfilter, serviceName, converter)) - - return result - - # same as get_values above, but then for one service only - def get_values_for_service(self, categoryfilter, servicename, converter=None): - deviceInstance = self.get_device_instance(servicename) - result = {} - - service = self.servicesByName[servicename] - - for category in categoryfilter: - - for path in service[category]: - - value, text, options = service.paths[path] - - if value is not None: - - value = value if converter is None else converter.convert(path, options['code'], value, text) - - precision = options.get('precision') - if precision: - value = round(value, precision) - - result[options['code'] + "[" + str(deviceInstance) + "]"] = value - - return result - - def track_value(self, serviceName, objectPath, callback, *args, **kwargs): - """ A DbusMonitor can watch specific service/path combos for changes - so that it is not fully reliant on the global handler_value_changes - in this class. Additional watches are deleted automatically when - the service disappears from dbus. """ - self.serviceWatches[serviceName].append( - self.dbusConn.add_signal_receiver( - partial(callback, *args, **kwargs), - dbus_interface='com.victronenergy.BusItem', - signal_name='PropertiesChanged', - path=objectPath, bus_name=serviceName)) - - -# ====== ALL CODE BELOW THIS LINE IS PURELY FOR DEVELOPING THIS CLASS ====== - -# Example function that can be used as a starting point to use this code -def value_changed_on_dbus(dbusServiceName, dbusPath, dict, changes, deviceInstance): - logger.debug("0 ----------------") - logger.debug("1 %s%s changed" % (dbusServiceName, dbusPath)) - logger.debug("2 vrm dict : %s" % dict) - logger.debug("3 changes-text: %s" % changes['Text']) - logger.debug("4 changes-value: %s" % changes['Value']) - logger.debug("5 deviceInstance: %s" % deviceInstance) - logger.debug("6 - end") - - -def nameownerchange(a, b): - # used to find memory leaks in dbusmonitor and VeDbusItemImport - import gc - gc.collect() - objects = gc.get_objects() - print len([o for o in objects if type(o).__name__ == 'VeDbusItemImport']) - print len([o for o in objects if type(o).__name__ == 'SignalMatch']) - print len(objects) - - -def print_values(dbusmonitor): - a = dbusmonitor.get_value('wrongservice', '/DbusInvalid', default_value=1000) - b = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/NotInTheMonitorList', default_value=1000) - c = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/DbusInvalid', default_value=1000) - d = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/NonExistingButMonitored', default_value=1000) - - print "All should be 1000: Wrong Service: %s, NotInTheMonitorList: %s, DbusInvalid: %s, NonExistingButMonitored: %s" % (a, b, c, d) - return True - -# We have a mainloop, but that is just for developing this code. Normally above class & code is used from -# some other class, such as vrmLogger or the pubsub Implementation. -def main(): - # Init logging - logging.basicConfig(level=logging.DEBUG) - logger.info(__file__ + " is starting up") - - # Have a mainloop, so we can send/receive asynchronous calls to and from dbus - DBusGMainLoop(set_as_default=True) - - import os - import sys - sys.path.insert(1, os.path.join(os.path.dirname(__file__), '../../')) - - dummy = {'code': None, 'whenToLog': 'configChange', 'accessLevel': None} - monitorlist = {'com.victronenergy.dummyservice': { - '/Connected': dummy, - '/ProductName': dummy, - '/Mgmt/Connection': dummy, - '/Dc/0/Voltage': dummy, - '/Dc/0/Current': dummy, - '/Dc/0/Temperature': dummy, - '/Load/I': dummy, - '/FirmwareVersion': dummy, - '/DbusInvalid': dummy, - '/NonExistingButMonitored': dummy}} - - d = DbusMonitor(monitorlist, value_changed_on_dbus, - deviceAddedCallback=nameownerchange, deviceRemovedCallback=nameownerchange) - - # logger.info("==configchange values==") - # logger.info(pprint.pformat(d.get_values(['configChange']))) - - # logger.info("==onIntervalAlways and onIntervalOnlyWhenChanged==") - # logger.info(pprint.pformat(d.get_values(['onIntervalAlways', 'onIntervalAlwaysAndOnEvent']))) - - gobject.timeout_add(1000, print_values, d) - - # Start and run the mainloop - logger.info("Starting mainloop, responding on only events") - mainloop = gobject.MainLoop() - mainloop.run() - -if __name__ == "__main__": - main() diff --git a/velib_python/v2.73/oldestVersion b/velib_python/v2.73/oldestVersion deleted file mode 100644 index c4da1d4..0000000 --- a/velib_python/v2.73/oldestVersion +++ /dev/null @@ -1 +0,0 @@ -v2.71 diff --git a/velib_python/v2.73/settingsdevice.py b/velib_python/v2.73/settingsdevice.py deleted file mode 100644 index b525375..0000000 --- a/velib_python/v2.73/settingsdevice.py +++ /dev/null @@ -1,115 +0,0 @@ -import dbus -import logging -import time -from functools import partial - -# Local imports -from vedbus import VeDbusItemImport - -## Indexes for the setting dictonary. -PATH = 0 -VALUE = 1 -MINIMUM = 2 -MAXIMUM = 3 -SILENT = 4 - -## The Settings Device class. -# Used by python programs, such as the vrm-logger, to read and write settings they -# need to store on disk. And since these settings might be changed from a different -# source, such as the GUI, the program can pass an eventCallback that will be called -# as soon as some setting is changed. -# -# The settings are stored in flash via the com.victronenergy.settings service on dbus. -# See https://github.com/victronenergy/localsettings for more info. -# -# If there are settings in de supportSettings list which are not yet on the dbus, -# and therefore not yet in the xml file, they will be added through the dbus-addSetting -# interface of com.victronenergy.settings. -class SettingsDevice(object): - ## The constructor processes the tree of dbus-items. - # @param bus the system-dbus object - # @param name the dbus-service-name of the settings dbus service, 'com.victronenergy.settings' - # @param supportedSettings dictionary with all setting-names, and their defaultvalue, min, max and whether - # the setting is silent. The 'silent' entry is optional. If set to true, no changes in the setting will - # be logged by localsettings. - # @param eventCallback function that will be called on changes on any of these settings - # @param timeout Maximum interval to wait for localsettings. An exception is thrown at the end of the - # interval if the localsettings D-Bus service has not appeared yet. - def __init__(self, bus, supportedSettings, eventCallback, name='com.victronenergy.settings', timeout=0): - logging.debug("===== Settings device init starting... =====") - self._bus = bus - self._dbus_name = name - self._eventCallback = eventCallback - self._values = {} # stored the values, used to pass the old value along on a setting change - self._settings = {} - - count = 0 - while True: - if 'com.victronenergy.settings' in self._bus.list_names(): - break - if count == timeout: - raise Exception("The settings service com.victronenergy.settings does not exist!") - count += 1 - logging.info('waiting for settings') - time.sleep(1) - - # Add the items. - for setting, options in supportedSettings.items(): - silent = len(options) > SILENT and options[SILENT] - busitem = self.addSetting(options[PATH], options[VALUE], - options[MINIMUM], options[MAXIMUM], silent, callback=partial(self.handleChangedSetting, setting)) - self._settings[setting] = busitem - self._values[setting] = busitem.get_value() - - logging.debug("===== Settings device init finished =====") - - def addSetting(self, path, value, _min, _max, silent=False, callback=None): - busitem = VeDbusItemImport(self._bus, self._dbus_name, path, callback) - if busitem.exists and (value, _min, _max, silent) == busitem._proxy.GetAttributes(): - logging.debug("Setting %s found" % path) - else: - logging.info("Setting %s does not exist yet or must be adjusted" % path) - - # Prepare to add the setting. Most dbus types extend the python - # type so it is only necessary to additionally test for Int64. - if isinstance(value, (int, dbus.Int64)): - itemType = 'i' - elif isinstance(value, float): - itemType = 'f' - else: - itemType = 's' - - # Add the setting - # TODO, make an object that inherits VeDbusItemImport, and complete the D-Bus settingsitem interface - settings_item = VeDbusItemImport(self._bus, self._dbus_name, '/Settings', createsignal=False) - setting_path = path.replace('/Settings/', '', 1) - if silent: - settings_item._proxy.AddSilentSetting('', setting_path, value, itemType, _min, _max) - else: - settings_item._proxy.AddSetting('', setting_path, value, itemType, _min, _max) - - busitem = VeDbusItemImport(self._bus, self._dbus_name, path, callback) - - return busitem - - def handleChangedSetting(self, setting, servicename, path, changes): - oldvalue = self._values[setting] if setting in self._values else None - self._values[setting] = changes['Value'] - - if self._eventCallback is None: - return - - self._eventCallback(setting, oldvalue, changes['Value']) - - def setDefault(self, path): - item = VeDbusItemImport(self._bus, self._dbus_name, path, createsignal=False) - item.set_default() - - def __getitem__(self, setting): - return self._settings[setting].get_value() - - def __setitem__(self, setting, newvalue): - result = self._settings[setting].set_value(newvalue) - if result != 0: - # Trying to make some false change to our own settings? How dumb! - assert False diff --git a/velib_python/v2.73/ve_utils.py b/velib_python/v2.73/ve_utils.py deleted file mode 100644 index c5cfb74..0000000 --- a/velib_python/v2.73/ve_utils.py +++ /dev/null @@ -1,253 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -from traceback import print_exc -from os import _exit as os_exit -from os import statvfs -from subprocess import check_output, CalledProcessError -import logging -import dbus -logger = logging.getLogger(__name__) - -VEDBUS_INVALID = dbus.Array([], signature=dbus.Signature('i'), variant_level=1) - -# Use this function to make sure the code quits on an unexpected exception. Make sure to use it -# when using gobject.idle_add and also gobject.timeout_add. -# Without this, the code will just keep running, since gobject does not stop the mainloop on an -# exception. -# Example: gobject.idle_add(exit_on_error, myfunc, arg1, arg2) -def exit_on_error(func, *args, **kwargs): - try: - return func(*args, **kwargs) - except: - try: - print 'exit_on_error: there was an exception. Printing stacktrace will be tried and then exit' - print_exc() - except: - pass - - # sys.exit() is not used, since that throws an exception, which does not lead to a program - # halt when used in a dbus callback, see connection.py in the Python/Dbus libraries, line 230. - os_exit(1) - - -__vrm_portal_id = None -def get_vrm_portal_id(): - # For the CCGX, the definition of the VRM Portal ID is that it is the mac address of the onboard- - # ethernet port (eth0), stripped from its colons (:) and lower case. - - # nice coincidence is that this also works fine when running on your (linux) development computer. - - global __vrm_portal_id - - if __vrm_portal_id: - return __vrm_portal_id - - # First try the method that works if we don't have a data partition. This will fail - # when the current user is not root. - try: - __vrm_portal_id = check_output("/sbin/get-unique-id").strip() - return __vrm_portal_id - except (CalledProcessError, OSError): - pass - - # Attempt to get the id from /data/venus/unique-id where venus puts it - # on startup. - try: - __vrm_portal_id = open('/data/venus/unique-id').read().strip() - except IOError: - pass - else: - return __vrm_portal_id - - # Fall back to getting our id using a syscall. Assume we are on linux. - # Allow the user to override what interface is used using an environment - # variable. - import fcntl, socket, struct, os - - iface = os.environ.get('VRM_IFACE', 'eth0') - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', iface[:15])) - __vrm_portal_id = ''.join(['%02x' % ord(char) for char in info[18:24]]) - return __vrm_portal_id - - -# See VE.Can registers - public.docx for definition of this conversion -def convert_vreg_version_to_readable(version): - def str_to_arr(x, length): - a = [] - for i in range(0, len(x), length): - a.append(x[i:i+length]) - return a - - x = "%x" % version - x = x.upper() - - if len(x) == 5 or len(x) == 3 or len(x) == 1: - x = '0' + x - - a = str_to_arr(x, 2); - - # remove the first 00 if there are three bytes and it is 00 - if len(a) == 3 and a[0] == '00': - a.remove(0); - - # if we have two or three bytes now, and the first character is a 0, remove it - if len(a) >= 2 and a[0][0:1] == '0': - a[0] = a[0][1]; - - result = '' - for item in a: - result += ('.' if result != '' else '') + item - - - result = 'v' + result - - return result - - -def get_free_space(path): - result = -1 - - try: - s = statvfs(path) - result = s.f_frsize * s.f_bavail # Number of free bytes that ordinary users - except Exception, ex: - logger.info("Error while retrieving free space for path %s: %s" % (path, ex)) - - return result - - -def get_load_averages(): - c = read_file('/proc/loadavg') - return c.split(' ')[:3] - - -def _get_sysfs_machine_name(): - try: - with open('/sys/firmware/devicetree/base/model', 'r') as f: - return f.read().rstrip('\x00') - except IOError: - pass - - return None - -# Returns None if it cannot find a machine name. Otherwise returns the string -# containing the name -def get_machine_name(): - # First try calling the venus utility script - try: - return check_output("/usr/bin/product-name").strip() - except (CalledProcessError, OSError): - pass - - # Fall back to sysfs - name = _get_sysfs_machine_name() - if name is not None: - return name - - # Fall back to venus build machine name - try: - with open('/etc/venus/machine', 'r') as f: - return f.read().strip() - except IOError: - pass - - return None - - -def get_product_id(): - """ Find the machine ID and return it. """ - - # First try calling the venus utility script - try: - return check_output("/usr/bin/product-id").strip() - except (CalledProcessError, OSError): - pass - - # Fall back machine name mechanism - name = _get_sysfs_machine_name() - return { - 'Color Control GX': 'C001', - 'Venus GX': 'C002', - 'Octo GX': 'C006', - 'EasySolar-II': 'C007', - 'MultiPlus-II': 'C008' - }.get(name, 'C003') # C003 is Generic - - -# Returns False if it cannot open the file. Otherwise returns its rstripped contents -def read_file(path): - content = False - - try: - with open(path, 'r') as f: - content = f.read().rstrip() - except Exception, ex: - logger.debug("Error while reading %s: %s" % (path, ex)) - - return content - - -def wrap_dbus_value(value): - if value is None: - return VEDBUS_INVALID - if isinstance(value, float): - return dbus.Double(value, variant_level=1) - if isinstance(value, bool): - return dbus.Boolean(value, variant_level=1) - if isinstance(value, int): - try: - return dbus.Int32(value, variant_level=1) - except OverflowError: - return dbus.Int64(value, variant_level=1) - if isinstance(value, str): - return dbus.String(value, variant_level=1) - if isinstance(value, unicode): - return dbus.String(value, variant_level=1) - if isinstance(value, list): - if len(value) == 0: - # If the list is empty we cannot infer the type of the contents. So assume unsigned integer. - # A (signed) integer is dangerous, because an empty list of signed integers is used to encode - # an invalid value. - return dbus.Array([], signature=dbus.Signature('u'), variant_level=1) - return dbus.Array([wrap_dbus_value(x) for x in value], variant_level=1) - if isinstance(value, long): - return dbus.Int64(value, variant_level=1) - if isinstance(value, dict): - # Wrapping the keys of the dictionary causes D-Bus errors like: - # 'arguments to dbus_message_iter_open_container() were incorrect, - # assertion "(type == DBUS_TYPE_ARRAY && contained_signature && - # *contained_signature == DBUS_DICT_ENTRY_BEGIN_CHAR) || (contained_signature == NULL || - # _dbus_check_is_valid_signature (contained_signature))" failed in file ...' - return dbus.Dictionary({(k, wrap_dbus_value(v)) for k, v in value.items()}, variant_level=1) - return value - - -dbus_int_types = (dbus.Int32, dbus.UInt32, dbus.Byte, dbus.Int16, dbus.UInt16, dbus.UInt32, dbus.Int64, dbus.UInt64) - - -def unwrap_dbus_value(val): - """Converts D-Bus values back to the original type. For example if val is of type DBus.Double, - a float will be returned.""" - if isinstance(val, dbus_int_types): - return int(val) - if isinstance(val, dbus.Double): - return float(val) - if isinstance(val, dbus.Array): - v = [unwrap_dbus_value(x) for x in val] - return None if len(v) == 0 else v - if isinstance(val, (dbus.Signature, dbus.String)): - return unicode(val) - # Python has no byte type, so we convert to an integer. - if isinstance(val, dbus.Byte): - return int(val) - if isinstance(val, dbus.ByteArray): - return "".join([str(x) for x in val]) - if isinstance(val, (list, tuple)): - return [unwrap_dbus_value(x) for x in val] - if isinstance(val, (dbus.Dictionary, dict)): - # Do not unwrap the keys, see comment in wrap_dbus_value - return dict([(x, unwrap_dbus_value(y)) for x, y in val.items()]) - if isinstance(val, dbus.Boolean): - return bool(val) - return val diff --git a/velib_python/v2.73/vedbus.py b/velib_python/v2.73/vedbus.py deleted file mode 100644 index 7fbe55d..0000000 --- a/velib_python/v2.73/vedbus.py +++ /dev/null @@ -1,501 +0,0 @@ -#!/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 - - ## Resets the item to its default value - def set_default(self): - self._proxy.SetDefault() - self._refreshcachedvalue() - - ## 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) diff --git a/velib_python/v2.94/dbusmonitor.py b/velib_python/v2.94/dbusmonitor.py deleted file mode 100644 index 5f8e153..0000000 --- a/velib_python/v2.94/dbusmonitor.py +++ /dev/null @@ -1,592 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -## @package dbus_vrm -# This code takes care of the D-Bus interface (not all of below is implemented yet): -# - on startup it scans the dbus for services we know. For each known service found, it searches for -# objects/paths we know. Everything we find is stored in items{}, and an event is registered: if a -# value changes weĺl be notified and can pass that on to our owner. For example the vrmLogger. -# we know. -# - after startup, it continues to monitor the dbus: -# 1) when services are added we do the same check on that -# 2) when services are removed, we remove any items that we had that referred to that service -# 3) if an existing services adds paths we update ourselves as well: on init, we make a -# VeDbusItemImport for a non-, or not yet existing objectpaths as well1 -# -# Code is used by the vrmLogger, and also the pubsub code. Both are other modules in the dbus_vrm repo. - -from dbus.mainloop.glib import DBusGMainLoop -from gi.repository import GLib -import dbus -import dbus.service -import inspect -import logging -import argparse -import pprint -import traceback -import os -from collections import defaultdict -from functools import partial - -# our own packages -from ve_utils import exit_on_error, wrap_dbus_value, unwrap_dbus_value -notfound = object() # For lookups where None is a valid result - -logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) -class SystemBus(dbus.bus.BusConnection): - def __new__(cls): - return dbus.bus.BusConnection.__new__(cls, dbus.bus.BusConnection.TYPE_SYSTEM) - -class SessionBus(dbus.bus.BusConnection): - def __new__(cls): - return dbus.bus.BusConnection.__new__(cls, dbus.bus.BusConnection.TYPE_SESSION) - -class MonitoredValue(object): - def __init__(self, value, text, options): - super(MonitoredValue, self).__init__() - self.value = value - self.text = text - self.options = options - - # For legacy code, allow treating this as a tuple/list - def __iter__(self): - return iter((self.value, self.text, self.options)) - -class Service(object): - whentologoptions = ['configChange', 'onIntervalAlwaysAndOnEvent', - 'onIntervalOnlyWhenChanged', 'onIntervalAlways', 'never'] - def __init__(self, id, serviceName, deviceInstance): - super(Service, self).__init__() - self.id = id - self.name = serviceName - self.paths = {} - self._seen = set() - self.deviceInstance = deviceInstance - - self.configChange = [] - self.onIntervalAlwaysAndOnEvent = [] - self.onIntervalOnlyWhenChanged = [] - self.onIntervalAlways = [] - self.never = [] - - # For legacy code, attributes can still be accessed as if keys from a - # dictionary. - def __setitem__(self, key, value): - self.__dict__[key] = value - def __getitem__(self, key): - return self.__dict__[key] - - def set_seen(self, path): - self._seen.add(path) - - def seen(self, path): - return path in self._seen - - @property - def service_class(self): - return '.'.join(self.name.split('.')[:3]) - -class DbusMonitor(object): - ## Constructor - def __init__(self, dbusTree, valueChangedCallback=None, deviceAddedCallback=None, - deviceRemovedCallback=None, vebusDeviceInstance0=False): - # valueChangedCallback is the callback that we call when something has changed. - # def value_changed_on_dbus(dbusServiceName, dbusPath, options, changes, deviceInstance): - # in which changes is a tuple with GetText() and GetValue() - self.valueChangedCallback = valueChangedCallback - self.deviceAddedCallback = deviceAddedCallback - self.deviceRemovedCallback = deviceRemovedCallback - self.dbusTree = dbusTree - self.vebusDeviceInstance0 = vebusDeviceInstance0 - - # Lists all tracked services. Stores name, id, device instance, value per path, and whenToLog info - # indexed by service name (eg. com.victronenergy.settings). - self.servicesByName = {} - - # Same values as self.servicesByName, but indexed by service id (eg. :1.30) - self.servicesById = {} - - # Keep track of services by class to speed up calls to get_service_list - self.servicesByClass = defaultdict(list) - - # Keep track of any additional watches placed on items - self.serviceWatches = defaultdict(list) - - # For a PC, connect to the SessionBus - # For a CCGX, connect to the SystemBus - self.dbusConn = SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else SystemBus() - - # subscribe to NameOwnerChange for bus connect / disconnect events. - (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ \ - else dbus.SystemBus()).add_signal_receiver( - self.dbus_name_owner_changed, - signal_name='NameOwnerChanged') - - # Subscribe to PropertiesChanged for all services - self.dbusConn.add_signal_receiver(self.handler_value_changes, - dbus_interface='com.victronenergy.BusItem', - signal_name='PropertiesChanged', path_keyword='path', - sender_keyword='senderId') - - # Subscribe to ItemsChanged for all services - self.dbusConn.add_signal_receiver(self.handler_item_changes, - dbus_interface='com.victronenergy.BusItem', - signal_name='ItemsChanged', path='/', - sender_keyword='senderId') - - logger.info('===== Search on dbus for services that we will monitor starting... =====') - serviceNames = self.dbusConn.list_names() - for serviceName in serviceNames: - self.scan_dbus_service(serviceName) - - logger.info('===== Search on dbus for services that we will monitor finished =====') - - def dbus_name_owner_changed(self, name, oldowner, newowner): - if not name.startswith("com.victronenergy."): - return - - #decouple, and process in main loop - GLib.idle_add(exit_on_error, self._process_name_owner_changed, name, oldowner, newowner) - - def _process_name_owner_changed(self, name, oldowner, newowner): - if newowner != '': - # so we found some new service. Check if we can do something with it. - newdeviceadded = self.scan_dbus_service(name) - if newdeviceadded and self.deviceAddedCallback is not None: - self.deviceAddedCallback(name, self.get_device_instance(name)) - - elif name in self.servicesByName: - # it disappeared, we need to remove it. - logger.info("%s disappeared from the dbus. Removing it from our lists" % name) - service = self.servicesByName[name] - deviceInstance = service['deviceInstance'] - del self.servicesById[service.id] - del self.servicesByName[name] - for watch in self.serviceWatches[name]: - watch.remove() - del self.serviceWatches[name] - self.servicesByClass[service.service_class].remove(service) - if self.deviceRemovedCallback is not None: - self.deviceRemovedCallback(name, deviceInstance) - - def scan_dbus_service(self, serviceName): - try: - return self.scan_dbus_service_inner(serviceName) - except: - logger.error("Ignoring %s because of error while scanning:" % (serviceName)) - traceback.print_exc() - return False - - # Errors 'org.freedesktop.DBus.Error.ServiceUnknown' and - # 'org.freedesktop.DBus.Error.Disconnected' seem to happen when the service - # disappears while its being scanned. Which might happen, but is not really - # normal either, so letting them go into the logs. - - # Scans the given dbus service to see if it contains anything interesting for us. If it does, add - # it to our list of monitored D-Bus services. - def scan_dbus_service_inner(self, serviceName): - - # make it a normal string instead of dbus string - serviceName = str(serviceName) - - paths = self.dbusTree.get('.'.join(serviceName.split('.')[0:3]), None) - if paths is None: - logger.debug("Ignoring service %s, not in the tree" % serviceName) - return False - - logger.info("Found: %s, scanning and storing items" % serviceName) - serviceId = self.dbusConn.get_name_owner(serviceName) - - # we should never be notified to add a D-Bus service that we already have. If this assertion - # raises, check process_name_owner_changed, and D-Bus workings. - assert serviceName not in self.servicesByName - assert serviceId not in self.servicesById - - # for vebus.ttyO1, this is workaround, since VRM Portal expects the main vebus - # devices at instance 0. Not sure how to fix this yet. - if serviceName == 'com.victronenergy.vebus.ttyO1' and self.vebusDeviceInstance0: - di = 0 - elif serviceName == 'com.victronenergy.settings': - di = 0 - elif serviceName.startswith('com.victronenergy.vecan.'): - di = 0 - else: - try: - di = self.dbusConn.call_blocking(serviceName, - '/DeviceInstance', None, 'GetValue', '', []) - except dbus.exceptions.DBusException: - logger.info(" %s was skipped because it has no device instance" % serviceName) - return False # Skip it - else: - di = int(di) - - logger.info(" %s has device instance %s" % (serviceName, di)) - service = Service(serviceId, serviceName, di) - - # Let's try to fetch everything in one go - values = {} - texts = {} - try: - values.update(self.dbusConn.call_blocking(serviceName, '/', None, 'GetValue', '', [])) - texts.update(self.dbusConn.call_blocking(serviceName, '/', None, 'GetText', '', [])) - except: - pass - - for path, options in paths.items(): - # path will be the D-Bus path: '/Ac/ActiveIn/L1/V' - # options will be a dictionary: {'code': 'V', 'whenToLog': 'onIntervalAlways'} - # check that the whenToLog setting is set to something we expect - assert options['whenToLog'] is None or options['whenToLog'] in Service.whentologoptions - - # Try to obtain the value we want from our bulk fetch. If we - # cannot find it there, do an individual query. - value = values.get(path[1:], notfound) - if value != notfound: - service.set_seen(path) - text = texts.get(path[1:], notfound) - if value is notfound or text is notfound: - try: - value = self.dbusConn.call_blocking(serviceName, path, None, 'GetValue', '', []) - service.set_seen(path) - text = self.dbusConn.call_blocking(serviceName, path, None, 'GetText', '', []) - except dbus.exceptions.DBusException as e: - if e.get_dbus_name() in ( - 'org.freedesktop.DBus.Error.ServiceUnknown', - 'org.freedesktop.DBus.Error.Disconnected'): - raise # This exception will be handled below - - # TODO org.freedesktop.DBus.Error.UnknownMethod really - # shouldn't happen but sometimes does. - logger.debug("%s %s does not exist (yet)" % (serviceName, path)) - value = None - text = None - - service.paths[path] = MonitoredValue(unwrap_dbus_value(value), unwrap_dbus_value(text), options) - - if options['whenToLog']: - service[options['whenToLog']].append(path) - - - logger.debug("Finished scanning and storing items for %s" % serviceName) - - # Adjust self at the end of the scan, so we don't have an incomplete set of - # data if an exception occurs during the scan. - self.servicesByName[serviceName] = service - self.servicesById[serviceId] = service - self.servicesByClass[service.service_class].append(service) - - return True - - def handler_item_changes(self, items, senderId): - if not isinstance(items, dict): - return - - try: - service = self.servicesById[senderId] - except KeyError: - # senderId isn't there, which means it hasn't been scanned yet. - return - - for path, changes in items.items(): - try: - v = unwrap_dbus_value(changes['Value']) - except (KeyError, TypeError): - continue - - try: - t = changes['Text'] - except KeyError: - t = str(v) - self._handler_value_changes(service, path, v, t) - - def handler_value_changes(self, changes, path, senderId): - # If this properyChange does not involve a value, our work is done. - if 'Value' not in changes: - return - - try: - service = self.servicesById[senderId] - except KeyError: - # senderId isn't there, which means it hasn't been scanned yet. - return - - v = unwrap_dbus_value(changes['Value']) - # Some services don't send Text with their PropertiesChanged events. - try: - t = changes['Text'] - except KeyError: - t = str(v) - self._handler_value_changes(service, path, v, t) - - def _handler_value_changes(self, service, path, value, text): - try: - a = service.paths[path] - except KeyError: - # path isn't there, which means it hasn't been scanned yet. - return - - service.set_seen(path) - - # First update our store to the new value - if a.value == value: - return - - a.value = value - a.text = text - - # And do the rest of the processing in on the mainloop - if self.valueChangedCallback is not None: - GLib.idle_add(exit_on_error, self._execute_value_changes, service.name, path, { - 'Value': value, 'Text': text}, a.options) - - def _execute_value_changes(self, serviceName, objectPath, changes, options): - # double check that the service still exists, as it might have - # disappeared between scheduling-for and executing this function. - if serviceName not in self.servicesByName: - return - - self.valueChangedCallback(serviceName, objectPath, - options, changes, self.get_device_instance(serviceName)) - - # Gets the value for a certain servicename and path - # The default_value is returned when: - # 1. When the service doesn't exist. - # 2. When the path asked for isn't being monitored. - # 3. When the path exists, but has dbus-invalid, ie an empty byte array. - # 4. When the path asked for is being monitored, but doesn't exist for that service. - def get_value(self, serviceName, objectPath, default_value=None): - service = self.servicesByName.get(serviceName, None) - if service is None: - return default_value - - value = service.paths.get(objectPath, None) - if value is None or value.value is None: - return default_value - - return value.value - - # returns if a dbus exists now, by doing a blocking dbus call. - # Typically seen will be sufficient and doesn't need access to the dbus. - def exists(self, serviceName, objectPath): - try: - self.dbusConn.call_blocking(serviceName, objectPath, None, 'GetValue', '', []) - return True - except dbus.exceptions.DBusException as e: - return False - - # Returns if there ever was a successful GetValue or valueChanged event. - # Unlike get_value this return True also if the actual value is invalid. - # - # Note: the path might no longer exists anymore, but that doesn't happen in - # practice. If a service really wants to reconfigure itself typically it should - # reconnect to the dbus which causes it to be rescanned and seen will be updated. - # If it is really needed to know if a path still exists, use exists. - def seen(self, serviceName, objectPath): - try: - return self.servicesByName[serviceName].seen(objectPath) - except KeyError: - return False - - # Sets the value for a certain servicename and path, returns the return value of the D-Bus SetValue - # method. If the underlying item does not exist (the service does not exist, or the objectPath was not - # registered) the function will return -1 - def set_value(self, serviceName, objectPath, value): - # Check if the D-Bus object referenced by serviceName and objectPath is registered. There is no - # necessity to do this, but it is in line with previous implementations which kept VeDbusItemImport - # objects for registers items only. - service = self.servicesByName.get(serviceName, None) - if service is None: - return -1 - if objectPath not in service.paths: - return -1 - # We do not catch D-Bus exceptions here, because the previous implementation did not do that either. - return self.dbusConn.call_blocking(serviceName, objectPath, - dbus_interface='com.victronenergy.BusItem', - method='SetValue', signature=None, - args=[wrap_dbus_value(value)]) - - # Similar to set_value, but operates asynchronously - def set_value_async(self, serviceName, objectPath, value, - reply_handler=None, error_handler=None): - service = self.servicesByName.get(serviceName, None) - if service is not None: - if objectPath in service.paths: - self.dbusConn.call_async(serviceName, objectPath, - dbus_interface='com.victronenergy.BusItem', - method='SetValue', signature=None, - args=[wrap_dbus_value(value)], - reply_handler=reply_handler, error_handler=error_handler) - return - - if error_handler is not None: - error_handler(TypeError('Service or path not found, ' - 'service=%s, path=%s' % (serviceName, objectPath))) - - # returns a dictionary, keys are the servicenames, value the instances - # optionally use the classfilter to get only a certain type of services, for - # example com.victronenergy.battery. - def get_service_list(self, classfilter=None): - if classfilter is None: - return { servicename: service.deviceInstance \ - for servicename, service in self.servicesByName.items() } - - if classfilter not in self.servicesByClass: - return {} - - return { service.name: service.deviceInstance \ - for service in self.servicesByClass[classfilter] } - - def get_device_instance(self, serviceName): - return self.servicesByName[serviceName].deviceInstance - - # Parameter categoryfilter is to be a list, containing the categories you want (configChange, - # onIntervalAlways, etc). - # Returns a dictionary, keys are codes + instance, in VRM querystring format. For example vvt[0]. And - # values are the value. - def get_values(self, categoryfilter, converter=None): - - result = {} - - for serviceName in self.servicesByName: - result.update(self.get_values_for_service(categoryfilter, serviceName, converter)) - - return result - - # same as get_values above, but then for one service only - def get_values_for_service(self, categoryfilter, servicename, converter=None): - deviceInstance = self.get_device_instance(servicename) - result = {} - - service = self.servicesByName[servicename] - - for category in categoryfilter: - - for path in service[category]: - - value, text, options = service.paths[path] - - if value is not None: - - value = value if converter is None else converter.convert(path, options['code'], value, text) - - precision = options.get('precision') - if precision: - value = round(value, precision) - - result[options['code'] + "[" + str(deviceInstance) + "]"] = value - - return result - - def track_value(self, serviceName, objectPath, callback, *args, **kwargs): - """ A DbusMonitor can watch specific service/path combos for changes - so that it is not fully reliant on the global handler_value_changes - in this class. Additional watches are deleted automatically when - the service disappears from dbus. """ - cb = partial(callback, *args, **kwargs) - - def root_tracker(items): - # Check if objectPath in dict - try: - v = items[objectPath] - _v = unwrap_dbus_value(v['Value']) - except (KeyError, TypeError): - return # not in this dict - - try: - t = v['Text'] - except KeyError: - cb({'Value': _v }) - else: - cb({'Value': _v, 'Text': t}) - - # Track changes on the path, and also on root - self.serviceWatches[serviceName].extend(( - self.dbusConn.add_signal_receiver(cb, - dbus_interface='com.victronenergy.BusItem', - signal_name='PropertiesChanged', - path=objectPath, bus_name=serviceName), - self.dbusConn.add_signal_receiver(root_tracker, - dbus_interface='com.victronenergy.BusItem', - signal_name='ItemsChanged', - path="/", bus_name=serviceName), - )) - - -# ====== ALL CODE BELOW THIS LINE IS PURELY FOR DEVELOPING THIS CLASS ====== - -# Example function that can be used as a starting point to use this code -def value_changed_on_dbus(dbusServiceName, dbusPath, dict, changes, deviceInstance): - logger.debug("0 ----------------") - logger.debug("1 %s%s changed" % (dbusServiceName, dbusPath)) - logger.debug("2 vrm dict : %s" % dict) - logger.debug("3 changes-text: %s" % changes['Text']) - logger.debug("4 changes-value: %s" % changes['Value']) - logger.debug("5 deviceInstance: %s" % deviceInstance) - logger.debug("6 - end") - - -def nameownerchange(a, b): - # used to find memory leaks in dbusmonitor and VeDbusItemImport - import gc - gc.collect() - objects = gc.get_objects() - print (len([o for o in objects if type(o).__name__ == 'VeDbusItemImport'])) - print (len([o for o in objects if type(o).__name__ == 'SignalMatch'])) - print (len(objects)) - - -def print_values(dbusmonitor): - a = dbusmonitor.get_value('wrongservice', '/DbusInvalid', default_value=1000) - b = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/NotInTheMonitorList', default_value=1000) - c = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/DbusInvalid', default_value=1000) - d = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/NonExistingButMonitored', default_value=1000) - - print ("All should be 1000: Wrong Service: %s, NotInTheMonitorList: %s, DbusInvalid: %s, NonExistingButMonitored: %s" % (a, b, c, d)) - return True - -# We have a mainloop, but that is just for developing this code. Normally above class & code is used from -# some other class, such as vrmLogger or the pubsub Implementation. -def main(): - # Init logging - logging.basicConfig(level=logging.DEBUG) - logger.info(__file__ + " is starting up") - - # Have a mainloop, so we can send/receive asynchronous calls to and from dbus - DBusGMainLoop(set_as_default=True) - - import os - import sys - sys.path.insert(1, os.path.join(os.path.dirname(__file__), '../../')) - - dummy = {'code': None, 'whenToLog': 'configChange', 'accessLevel': None} - monitorlist = {'com.victronenergy.dummyservice': { - '/Connected': dummy, - '/ProductName': dummy, - '/Mgmt/Connection': dummy, - '/Dc/0/Voltage': dummy, - '/Dc/0/Current': dummy, - '/Dc/0/Temperature': dummy, - '/Load/I': dummy, - '/FirmwareVersion': dummy, - '/DbusInvalid': dummy, - '/NonExistingButMonitored': dummy}} - - d = DbusMonitor(monitorlist, value_changed_on_dbus, - deviceAddedCallback=nameownerchange, deviceRemovedCallback=nameownerchange) - - # logger.info("==configchange values==") - # logger.info(pprint.pformat(d.get_values(['configChange']))) - - # logger.info("==onIntervalAlways and onIntervalOnlyWhenChanged==") - # logger.info(pprint.pformat(d.get_values(['onIntervalAlways', 'onIntervalAlwaysAndOnEvent']))) - - GLib.timeout_add(1000, print_values, d) - - # Start and run the mainloop - logger.info("Starting mainloop, responding on only events") - mainloop = GLib.MainLoop() - mainloop.run() - -if __name__ == "__main__": - main() diff --git a/velib_python/v2.94/oldestVersion b/velib_python/v2.94/oldestVersion deleted file mode 100644 index 366edb3..0000000 --- a/velib_python/v2.94/oldestVersion +++ /dev/null @@ -1 +0,0 @@ -v2.80 diff --git a/velib_python/v2.94/settingsdevice.py b/velib_python/v2.94/settingsdevice.py deleted file mode 100644 index a207e8b..0000000 --- a/velib_python/v2.94/settingsdevice.py +++ /dev/null @@ -1,118 +0,0 @@ -import dbus -import logging -import time -from functools import partial - -# Local imports -from vedbus import VeDbusItemImport - -## Indexes for the setting dictonary. -PATH = 0 -VALUE = 1 -MINIMUM = 2 -MAXIMUM = 3 -SILENT = 4 - -## The Settings Device class. -# Used by python programs, such as the vrm-logger, to read and write settings they -# need to store on disk. And since these settings might be changed from a different -# source, such as the GUI, the program can pass an eventCallback that will be called -# as soon as some setting is changed. -# -# The settings are stored in flash via the com.victronenergy.settings service on dbus. -# See https://github.com/victronenergy/localsettings for more info. -# -# If there are settings in de supportSettings list which are not yet on the dbus, -# and therefore not yet in the xml file, they will be added through the dbus-addSetting -# interface of com.victronenergy.settings. -class SettingsDevice(object): - ## The constructor processes the tree of dbus-items. - # @param bus the system-dbus object - # @param name the dbus-service-name of the settings dbus service, 'com.victronenergy.settings' - # @param supportedSettings dictionary with all setting-names, and their defaultvalue, min, max and whether - # the setting is silent. The 'silent' entry is optional. If set to true, no changes in the setting will - # be logged by localsettings. - # @param eventCallback function that will be called on changes on any of these settings - # @param timeout Maximum interval to wait for localsettings. An exception is thrown at the end of the - # interval if the localsettings D-Bus service has not appeared yet. - def __init__(self, bus, supportedSettings, eventCallback, name='com.victronenergy.settings', timeout=0): - logging.debug("===== Settings device init starting... =====") - self._bus = bus - self._dbus_name = name - self._eventCallback = eventCallback - self._values = {} # stored the values, used to pass the old value along on a setting change - self._settings = {} - - count = 0 - while True: - if 'com.victronenergy.settings' in self._bus.list_names(): - break - if count == timeout: - raise Exception("The settings service com.victronenergy.settings does not exist!") - count += 1 - logging.info('waiting for settings') - time.sleep(1) - - # Add the items. - self.addSettings(supportedSettings) - - logging.debug("===== Settings device init finished =====") - - def addSettings(self, settings): - for setting, options in settings.items(): - silent = len(options) > SILENT and options[SILENT] - busitem = self.addSetting(options[PATH], options[VALUE], - options[MINIMUM], options[MAXIMUM], silent, callback=partial(self.handleChangedSetting, setting)) - self._settings[setting] = busitem - self._values[setting] = busitem.get_value() - - def addSetting(self, path, value, _min, _max, silent=False, callback=None): - busitem = VeDbusItemImport(self._bus, self._dbus_name, path, callback) - if busitem.exists and (value, _min, _max, silent) == busitem._proxy.GetAttributes(): - logging.debug("Setting %s found" % path) - else: - logging.info("Setting %s does not exist yet or must be adjusted" % path) - - # Prepare to add the setting. Most dbus types extend the python - # type so it is only necessary to additionally test for Int64. - if isinstance(value, (int, dbus.Int64)): - itemType = 'i' - elif isinstance(value, float): - itemType = 'f' - else: - itemType = 's' - - # Add the setting - # TODO, make an object that inherits VeDbusItemImport, and complete the D-Bus settingsitem interface - settings_item = VeDbusItemImport(self._bus, self._dbus_name, '/Settings', createsignal=False) - setting_path = path.replace('/Settings/', '', 1) - if silent: - settings_item._proxy.AddSilentSetting('', setting_path, value, itemType, _min, _max) - else: - settings_item._proxy.AddSetting('', setting_path, value, itemType, _min, _max) - - busitem = VeDbusItemImport(self._bus, self._dbus_name, path, callback) - - return busitem - - def handleChangedSetting(self, setting, servicename, path, changes): - oldvalue = self._values[setting] if setting in self._values else None - self._values[setting] = changes['Value'] - - if self._eventCallback is None: - return - - self._eventCallback(setting, oldvalue, changes['Value']) - - def setDefault(self, path): - item = VeDbusItemImport(self._bus, self._dbus_name, path, createsignal=False) - item.set_default() - - def __getitem__(self, setting): - return self._settings[setting].get_value() - - def __setitem__(self, setting, newvalue): - result = self._settings[setting].set_value(newvalue) - if result != 0: - # Trying to make some false change to our own settings? How dumb! - assert False diff --git a/velib_python/v2.94/ve_utils.py b/velib_python/v2.94/ve_utils.py deleted file mode 100644 index e8d847d..0000000 --- a/velib_python/v2.94/ve_utils.py +++ /dev/null @@ -1,265 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -import sys -from traceback import print_exc -from os import _exit as os_exit -from os import statvfs -from subprocess import check_output, CalledProcessError -import logging -import dbus -logger = logging.getLogger(__name__) - -VEDBUS_INVALID = dbus.Array([], signature=dbus.Signature('i'), variant_level=1) - -class NoVrmPortalIdError(Exception): - pass - -# Use this function to make sure the code quits on an unexpected exception. Make sure to use it -# when using GLib.idle_add and also GLib.timeout_add. -# Without this, the code will just keep running, since GLib does not stop the mainloop on an -# exception. -# Example: GLib.idle_add(exit_on_error, myfunc, arg1, arg2) -def exit_on_error(func, *args, **kwargs): - try: - return func(*args, **kwargs) - except: - try: - print ('exit_on_error: there was an exception. Printing stacktrace will be tried and then exit') - print_exc() - except: - pass - - # sys.exit() is not used, since that throws an exception, which does not lead to a program - # halt when used in a dbus callback, see connection.py in the Python/Dbus libraries, line 230. - os_exit(1) - - -__vrm_portal_id = None -def get_vrm_portal_id(): - # The original definition of the VRM Portal ID is that it is the mac - # address of the onboard- ethernet port (eth0), stripped from its colons - # (:) and lower case. This may however differ between platforms. On Venus - # the task is therefore deferred to /sbin/get-unique-id so that a - # platform specific method can be easily defined. - # - # If /sbin/get-unique-id does not exist, then use the ethernet address - # of eth0. This also handles the case where velib_python is used as a - # package install on a Raspberry Pi. - # - # On a Linux host where the network interface may not be eth0, you can set - # the VRM_IFACE environment variable to the correct name. - - global __vrm_portal_id - - if __vrm_portal_id: - return __vrm_portal_id - - portal_id = None - - # First try the method that works if we don't have a data partition. This - # will fail when the current user is not root. - try: - portal_id = check_output("/sbin/get-unique-id").decode("utf-8", "ignore").strip() - if not portal_id: - raise NoVrmPortalIdError("get-unique-id returned blank") - __vrm_portal_id = portal_id - return portal_id - except CalledProcessError: - # get-unique-id returned non-zero - raise NoVrmPortalIdError("get-unique-id returned non-zero") - except OSError: - # File doesn't exist, use fallback - pass - - # Fall back to getting our id using a syscall. Assume we are on linux. - # Allow the user to override what interface is used using an environment - # variable. - import fcntl, socket, struct, os - - iface = os.environ.get('VRM_IFACE', 'eth0').encode('ascii') - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - try: - info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', iface[:15])) - except IOError: - raise NoVrmPortalIdError("ioctl failed for eth0") - - __vrm_portal_id = info[18:24].hex() - return __vrm_portal_id - - -# See VE.Can registers - public.docx for definition of this conversion -def convert_vreg_version_to_readable(version): - def str_to_arr(x, length): - a = [] - for i in range(0, len(x), length): - a.append(x[i:i+length]) - return a - - x = "%x" % version - x = x.upper() - - if len(x) == 5 or len(x) == 3 or len(x) == 1: - x = '0' + x - - a = str_to_arr(x, 2); - - # remove the first 00 if there are three bytes and it is 00 - if len(a) == 3 and a[0] == '00': - a.remove(0); - - # if we have two or three bytes now, and the first character is a 0, remove it - if len(a) >= 2 and a[0][0:1] == '0': - a[0] = a[0][1]; - - result = '' - for item in a: - result += ('.' if result != '' else '') + item - - - result = 'v' + result - - return result - - -def get_free_space(path): - result = -1 - - try: - s = statvfs(path) - result = s.f_frsize * s.f_bavail # Number of free bytes that ordinary users - except Exception as ex: - logger.info("Error while retrieving free space for path %s: %s" % (path, ex)) - - return result - - -def get_load_averages(): - c = read_file('/proc/loadavg') - return c.split(' ')[:3] - - -def _get_sysfs_machine_name(): - try: - with open('/sys/firmware/devicetree/base/model', 'r') as f: - return f.read().rstrip('\x00') - except IOError: - pass - - return None - -# Returns None if it cannot find a machine name. Otherwise returns the string -# containing the name -def get_machine_name(): - # First try calling the venus utility script - try: - return check_output("/usr/bin/product-name").strip().decode('UTF-8') - except (CalledProcessError, OSError): - pass - - # Fall back to sysfs - name = _get_sysfs_machine_name() - if name is not None: - return name - - # Fall back to venus build machine name - try: - with open('/etc/venus/machine', 'r', encoding='UTF-8') as f: - return f.read().strip() - except IOError: - pass - - return None - - -def get_product_id(): - """ Find the machine ID and return it. """ - - # First try calling the venus utility script - try: - return check_output("/usr/bin/product-id").strip() - except (CalledProcessError, OSError): - pass - - # Fall back machine name mechanism - name = _get_sysfs_machine_name() - return { - 'Color Control GX': 'C001', - 'Venus GX': 'C002', - 'Octo GX': 'C006', - 'EasySolar-II': 'C007', - 'MultiPlus-II': 'C008' - }.get(name, 'C003') # C003 is Generic - - -# Returns False if it cannot open the file. Otherwise returns its rstripped contents -def read_file(path): - content = False - - try: - with open(path, 'r') as f: - content = f.read().rstrip() - except Exception as ex: - logger.debug("Error while reading %s: %s" % (path, ex)) - - return content - - -def wrap_dbus_value(value): - if value is None: - return VEDBUS_INVALID - if isinstance(value, float): - return dbus.Double(value, variant_level=1) - if isinstance(value, bool): - return dbus.Boolean(value, variant_level=1) - if isinstance(value, int): - try: - return dbus.Int32(value, variant_level=1) - except OverflowError: - return dbus.Int64(value, variant_level=1) - if isinstance(value, str): - return dbus.String(value, variant_level=1) - if isinstance(value, list): - if len(value) == 0: - # If the list is empty we cannot infer the type of the contents. So assume unsigned integer. - # A (signed) integer is dangerous, because an empty list of signed integers is used to encode - # an invalid value. - return dbus.Array([], signature=dbus.Signature('u'), variant_level=1) - return dbus.Array([wrap_dbus_value(x) for x in value], variant_level=1) - if isinstance(value, dict): - # Wrapping the keys of the dictionary causes D-Bus errors like: - # 'arguments to dbus_message_iter_open_container() were incorrect, - # assertion "(type == DBUS_TYPE_ARRAY && contained_signature && - # *contained_signature == DBUS_DICT_ENTRY_BEGIN_CHAR) || (contained_signature == NULL || - # _dbus_check_is_valid_signature (contained_signature))" failed in file ...' - return dbus.Dictionary({(k, wrap_dbus_value(v)) for k, v in value.items()}, variant_level=1) - return value - - -dbus_int_types = (dbus.Int32, dbus.UInt32, dbus.Byte, dbus.Int16, dbus.UInt16, dbus.UInt32, dbus.Int64, dbus.UInt64) - - -def unwrap_dbus_value(val): - """Converts D-Bus values back to the original type. For example if val is of type DBus.Double, - a float will be returned.""" - if isinstance(val, dbus_int_types): - return int(val) - if isinstance(val, dbus.Double): - return float(val) - if isinstance(val, dbus.Array): - v = [unwrap_dbus_value(x) for x in val] - return None if len(v) == 0 else v - if isinstance(val, (dbus.Signature, dbus.String)): - return str(val) - # Python has no byte type, so we convert to an integer. - if isinstance(val, dbus.Byte): - return int(val) - if isinstance(val, dbus.ByteArray): - return "".join([bytes(x) for x in val]) - if isinstance(val, (list, tuple)): - return [unwrap_dbus_value(x) for x in val] - if isinstance(val, (dbus.Dictionary, dict)): - # Do not unwrap the keys, see comment in wrap_dbus_value - return dict([(x, unwrap_dbus_value(y)) for x, y in val.items()]) - if isinstance(val, dbus.Boolean): - return bool(val) - return val diff --git a/velib_python/v2.94/vedbus.py b/velib_python/v2.94/vedbus.py deleted file mode 100644 index d6dac60..0000000 --- a/velib_python/v2.94/vedbus.py +++ /dev/null @@ -1,599 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import dbus.service -import logging -import traceback -import os -import weakref -from collections import defaultdict -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 = {} - self._ratelimiters = [] - - # 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['/'] = VeDbusRootExport(self._dbusconn, '/', self) - - logging.info("registered ourselves on D-Bus as %s" % servicename) - - # To force immediate deregistering of this dbus service and all its object paths, explicitly - # call __del__(). - def __del__(self): - for node in list(self._dbusnodes.values()): - node.__del__() - self._dbusnodes.clear() - for item in list(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] = VeDbusTreeExport(self._dbusconn, subPath, self) - 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) - - # 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 list(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 - - def __enter__(self): - l = ServiceContext(self) - self._ratelimiters.append(l) - return l - - def __exit__(self, *exc): - # pop off the top one and flush it. If with statements are nested - # then each exit flushes its own part. - if self._ratelimiters: - self._ratelimiters.pop().flush() - -class ServiceContext(object): - def __init__(self, parent): - self.parent = parent - self.changes = {} - - def __getitem__(self, path): - return self.parent[path] - - def __setitem__(self, path, newvalue): - c = self.parent._dbusobjects[path]._local_set_value(newvalue) - if c is not None: - self.changes[path] = c - - def flush(self): - if self.changes: - self.parent._dbusnodes['/'].ItemsChanged(self.changes) - -class TrackerDict(defaultdict): - """ Same as defaultdict, but passes the key to default_factory. """ - def __missing__(self, key): - self[key] = x = self.default_factory(key) - return x - -class VeDbusRootTracker(object): - """ This tracks the root of a dbus path and listens for PropertiesChanged - signals. When a signal arrives, parse it and unpack the key/value changes - into traditional events, then pass it to the original eventCallback - method. """ - def __init__(self, bus, serviceName): - self.importers = defaultdict(weakref.WeakSet) - self.serviceName = serviceName - self._match = bus.get_object(serviceName, '/', introspect=False).connect_to_signal( - "ItemsChanged", weak_functor(self._items_changed_handler)) - - def __del__(self): - self._match.remove() - self._match = None - - def add(self, i): - self.importers[i.path].add(i) - - def _items_changed_handler(self, items): - if not isinstance(items, dict): - return - - for path, changes in items.items(): - try: - v = changes['Value'] - except KeyError: - continue - - try: - t = changes['Text'] - except KeyError: - t = str(unwrap_dbus_value(v)) - - for i in self.importers.get(path, ()): - i._properties_changed_handler({'Value': v, 'Text': t}) - -""" -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): - def __new__(cls, bus, serviceName, path, eventCallback=None, createsignal=True): - instance = object.__new__(cls) - - # If signal tracking should be done, also add to root tracker - if createsignal: - if "_roots" not in cls.__dict__: - cls._roots = TrackerDict(lambda k: VeDbusRootTracker(bus, k)) - - return instance - - ## 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)) - self._roots[serviceName].add(self) - - # 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 is not 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 - - ## Resets the item to its default value - def set_default(self): - self._proxy.SetDefault() - self._refreshcachedvalue() - - ## 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, service): - dbus.service.Object.__init__(self, bus, objectPath) - self._service = service - 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] - - def _get_value_handler(self, path, get_text=False): - logging.debug("_get_value_handler called for %s" % path) - r = {} - px = path - if not px.endswith('/'): - px += '/' - for p, item in self._service._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 - - @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 VeDbusRootExport(VeDbusTreeExport): - @dbus.service.signal('com.victronenergy.BusItem', signature='a{sa{sv}}') - def ItemsChanged(self, changes): - pass - - @dbus.service.method('com.victronenergy.BusItem', out_signature='a{sa{sv}}') - def GetItems(self): - return { - path: { - 'Value': wrap_dbus_value(item.local_get_value()), - 'Text': item.GetText() } - for path, item in self._service._dbusobjects.items() - } - - -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): - changes = self._local_set_value(newvalue) - if changes is not None: - self.PropertiesChanged(changes) - - def _local_set_value(self, newvalue): - if self._value == newvalue: - return None - - self._value = newvalue - return { - 'Value': wrap_dbus_value(newvalue), - 'Text': self.GetText() - } - - 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) diff --git a/velib_python/v3.40~37/dbusmonitor.py b/velib_python/v3.40~37/dbusmonitor.py deleted file mode 100644 index cb2185d..0000000 --- a/velib_python/v3.40~37/dbusmonitor.py +++ /dev/null @@ -1,554 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -## @package dbus_vrm -# This code takes care of the D-Bus interface (not all of below is implemented yet): -# - on startup it scans the dbus for services we know. For each known service found, it searches for -# objects/paths we know. Everything we find is stored in items{}, and an event is registered: if a -# value changes weĺl be notified and can pass that on to our owner. For example the vrmLogger. -# we know. -# - after startup, it continues to monitor the dbus: -# 1) when services are added we do the same check on that -# 2) when services are removed, we remove any items that we had that referred to that service -# 3) if an existing services adds paths we update ourselves as well: on init, we make a -# VeDbusItemImport for a non-, or not yet existing objectpaths as well1 -# -# Code is used by the vrmLogger, and also the pubsub code. Both are other modules in the dbus_vrm repo. - -from dbus.mainloop.glib import DBusGMainLoop -from gi.repository import GLib -import dbus -import dbus.service -import inspect -import logging -import argparse -import pprint -import traceback -import os -from collections import defaultdict -from functools import partial - -# our own packages -from ve_utils import exit_on_error, wrap_dbus_value, unwrap_dbus_value -notfound = object() # For lookups where None is a valid result - -logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) -class SystemBus(dbus.bus.BusConnection): - def __new__(cls): - return dbus.bus.BusConnection.__new__(cls, dbus.bus.BusConnection.TYPE_SYSTEM) - -class SessionBus(dbus.bus.BusConnection): - def __new__(cls): - return dbus.bus.BusConnection.__new__(cls, dbus.bus.BusConnection.TYPE_SESSION) - -class MonitoredValue(object): - def __init__(self, value, text, options): - super(MonitoredValue, self).__init__() - self.value = value - self.text = text - self.options = options - - # For legacy code, allow treating this as a tuple/list - def __iter__(self): - return iter((self.value, self.text, self.options)) - -class Service(object): - def __init__(self, id, serviceName, deviceInstance): - super(Service, self).__init__() - self.id = id - self.name = serviceName - self.paths = {} - self._seen = set() - self.deviceInstance = deviceInstance - - # For legacy code, attributes can still be accessed as if keys from a - # dictionary. - def __setitem__(self, key, value): - self.__dict__[key] = value - def __getitem__(self, key): - return self.__dict__[key] - - def set_seen(self, path): - self._seen.add(path) - - def seen(self, path): - return path in self._seen - - @property - def service_class(self): - return '.'.join(self.name.split('.')[:3]) - -class DbusMonitor(object): - ## Constructor - def __init__(self, dbusTree, valueChangedCallback=None, deviceAddedCallback=None, - deviceRemovedCallback=None, namespace="com.victronenergy"): - # valueChangedCallback is the callback that we call when something has changed. - # def value_changed_on_dbus(dbusServiceName, dbusPath, options, changes, deviceInstance): - # in which changes is a tuple with GetText() and GetValue() - self.valueChangedCallback = valueChangedCallback - self.deviceAddedCallback = deviceAddedCallback - self.deviceRemovedCallback = deviceRemovedCallback - self.dbusTree = dbusTree - - # Lists all tracked services. Stores name, id, device instance, value per path, and whenToLog info - # indexed by service name (eg. com.victronenergy.settings). - self.servicesByName = {} - - # Same values as self.servicesByName, but indexed by service id (eg. :1.30) - self.servicesById = {} - - # Keep track of services by class to speed up calls to get_service_list - self.servicesByClass = defaultdict(list) - - # Keep track of any additional watches placed on items - self.serviceWatches = defaultdict(list) - - # For a PC, connect to the SessionBus - # For a CCGX, connect to the SystemBus - self.dbusConn = SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else SystemBus() - - # subscribe to NameOwnerChange for bus connect / disconnect events. - # NOTE: this is on a different bus then the one above! - standardBus = (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ \ - else dbus.SystemBus()) - - self.add_name_owner_changed_receiver(standardBus, self.dbus_name_owner_changed) - - # Subscribe to PropertiesChanged for all services - self.dbusConn.add_signal_receiver(self.handler_value_changes, - dbus_interface='com.victronenergy.BusItem', - signal_name='PropertiesChanged', path_keyword='path', - sender_keyword='senderId') - - # Subscribe to ItemsChanged for all services - self.dbusConn.add_signal_receiver(self.handler_item_changes, - dbus_interface='com.victronenergy.BusItem', - signal_name='ItemsChanged', path='/', - sender_keyword='senderId') - - logger.info('===== Search on dbus for services that we will monitor starting... =====') - serviceNames = self.dbusConn.list_names() - for serviceName in serviceNames: - self.scan_dbus_service(serviceName) - - logger.info('===== Search on dbus for services that we will monitor finished =====') - - @staticmethod - def make_service(serviceId, serviceName, deviceInstance): - """ Override this to use a different kind of service object. """ - return Service(serviceId, serviceName, deviceInstance) - - def make_monitor(self, service, path, value, text, options): - """ Override this to do more things with monitoring. """ - return MonitoredValue(unwrap_dbus_value(value), unwrap_dbus_value(text), options) - - def dbus_name_owner_changed(self, name, oldowner, newowner): - if not name.startswith("com.victronenergy."): - return - - #decouple, and process in main loop - GLib.idle_add(exit_on_error, self._process_name_owner_changed, name, oldowner, newowner) - - @staticmethod - # When supported, only name owner changes for the the given namespace are reported. This - # prevents spending cpu time at irrelevant changes, like scripts accessing the bus temporarily. - def add_name_owner_changed_receiver(dbus, name_owner_changed, namespace="com.victronenergy"): - # support for arg0namespace is submitted upstream, but not included at the time of - # writing, Venus OS does support it, so try if it works. - if namespace is None: - dbus.add_signal_receiver(name_owner_changed, signal_name='NameOwnerChanged') - else: - try: - dbus.add_signal_receiver(name_owner_changed, - signal_name='NameOwnerChanged', arg0namespace=namespace) - except TypeError: - dbus.add_signal_receiver(name_owner_changed, signal_name='NameOwnerChanged') - - def _process_name_owner_changed(self, name, oldowner, newowner): - if newowner != '': - # so we found some new service. Check if we can do something with it. - newdeviceadded = self.scan_dbus_service(name) - if newdeviceadded and self.deviceAddedCallback is not None: - self.deviceAddedCallback(name, self.get_device_instance(name)) - - elif name in self.servicesByName: - # it disappeared, we need to remove it. - logger.info("%s disappeared from the dbus. Removing it from our lists" % name) - service = self.servicesByName[name] - del self.servicesById[service.id] - del self.servicesByName[name] - for watch in self.serviceWatches[name]: - watch.remove() - del self.serviceWatches[name] - self.servicesByClass[service.service_class].remove(service) - if self.deviceRemovedCallback is not None: - self.deviceRemovedCallback(name, service.deviceInstance) - - def scan_dbus_service(self, serviceName): - try: - return self.scan_dbus_service_inner(serviceName) - except: - logger.error("Ignoring %s because of error while scanning:" % (serviceName)) - traceback.print_exc() - return False - - # Errors 'org.freedesktop.DBus.Error.ServiceUnknown' and - # 'org.freedesktop.DBus.Error.Disconnected' seem to happen when the service - # disappears while its being scanned. Which might happen, but is not really - # normal either, so letting them go into the logs. - - # Scans the given dbus service to see if it contains anything interesting for us. If it does, add - # it to our list of monitored D-Bus services. - def scan_dbus_service_inner(self, serviceName): - - # make it a normal string instead of dbus string - serviceName = str(serviceName) - - paths = self.dbusTree.get('.'.join(serviceName.split('.')[0:3]), None) - if paths is None: - logger.debug("Ignoring service %s, not in the tree" % serviceName) - return False - - logger.info("Found: %s, scanning and storing items" % serviceName) - serviceId = self.dbusConn.get_name_owner(serviceName) - - # we should never be notified to add a D-Bus service that we already have. If this assertion - # raises, check process_name_owner_changed, and D-Bus workings. - assert serviceName not in self.servicesByName - assert serviceId not in self.servicesById - - if serviceName == 'com.victronenergy.settings': - di = 0 - elif serviceName.startswith('com.victronenergy.vecan.'): - di = 0 - else: - try: - di = self.dbusConn.call_blocking(serviceName, - '/DeviceInstance', None, 'GetValue', '', []) - except dbus.exceptions.DBusException: - logger.info(" %s was skipped because it has no device instance" % serviceName) - return False # Skip it - else: - di = int(di) - - logger.info(" %s has device instance %s" % (serviceName, di)) - service = self.make_service(serviceId, serviceName, di) - - # Let's try to fetch everything in one go - values = {} - texts = {} - try: - values.update(self.dbusConn.call_blocking(serviceName, '/', None, 'GetValue', '', [])) - texts.update(self.dbusConn.call_blocking(serviceName, '/', None, 'GetText', '', [])) - except: - pass - - for path, options in paths.items(): - # path will be the D-Bus path: '/Ac/ActiveIn/L1/V' - # options will be a dictionary: {'code': 'V', 'whenToLog': 'onIntervalAlways'} - - # Try to obtain the value we want from our bulk fetch. If we - # cannot find it there, do an individual query. - value = values.get(path[1:], notfound) - if value != notfound: - service.set_seen(path) - text = texts.get(path[1:], notfound) - if value is notfound or text is notfound: - try: - value = self.dbusConn.call_blocking(serviceName, path, None, 'GetValue', '', []) - service.set_seen(path) - text = self.dbusConn.call_blocking(serviceName, path, None, 'GetText', '', []) - except dbus.exceptions.DBusException as e: - if e.get_dbus_name() in ( - 'org.freedesktop.DBus.Error.ServiceUnknown', - 'org.freedesktop.DBus.Error.Disconnected'): - raise # This exception will be handled below - - # TODO org.freedesktop.DBus.Error.UnknownMethod really - # shouldn't happen but sometimes does. - logger.debug("%s %s does not exist (yet)" % (serviceName, path)) - value = None - text = None - - service.paths[path] = self.make_monitor(service, path, unwrap_dbus_value(value), unwrap_dbus_value(text), options) - - - logger.debug("Finished scanning and storing items for %s" % serviceName) - - # Adjust self at the end of the scan, so we don't have an incomplete set of - # data if an exception occurs during the scan. - self.servicesByName[serviceName] = service - self.servicesById[serviceId] = service - self.servicesByClass[service.service_class].append(service) - - return True - - def handler_item_changes(self, items, senderId): - if not isinstance(items, dict): - return - - try: - service = self.servicesById[senderId] - except KeyError: - # senderId isn't there, which means it hasn't been scanned yet. - return - - for path, changes in items.items(): - try: - v = unwrap_dbus_value(changes['Value']) - except (KeyError, TypeError): - continue - - try: - t = changes['Text'] - except KeyError: - t = str(v) - self._handler_value_changes(service, path, v, t) - - def handler_value_changes(self, changes, path, senderId): - # If this properyChange does not involve a value, our work is done. - if 'Value' not in changes: - return - - try: - service = self.servicesById[senderId] - except KeyError: - # senderId isn't there, which means it hasn't been scanned yet. - return - - v = unwrap_dbus_value(changes['Value']) - # Some services don't send Text with their PropertiesChanged events. - try: - t = changes['Text'] - except KeyError: - t = str(v) - self._handler_value_changes(service, path, v, t) - - def _handler_value_changes(self, service, path, value, text): - try: - a = service.paths[path] - except KeyError: - # path isn't there, which means it hasn't been scanned yet. - return - - service.set_seen(path) - - # First update our store to the new value - if a.value == value: - return - - a.value = value - a.text = text - - # And do the rest of the processing in on the mainloop - if self.valueChangedCallback is not None: - GLib.idle_add(exit_on_error, self._execute_value_changes, service.name, path, { - 'Value': value, 'Text': text}, a.options) - - def _execute_value_changes(self, serviceName, objectPath, changes, options): - # double check that the service still exists, as it might have - # disappeared between scheduling-for and executing this function. - if serviceName not in self.servicesByName: - return - - self.valueChangedCallback(serviceName, objectPath, - options, changes, self.get_device_instance(serviceName)) - - # Gets the value for a certain servicename and path - # The default_value is returned when: - # 1. When the service doesn't exist. - # 2. When the path asked for isn't being monitored. - # 3. When the path exists, but has dbus-invalid, ie an empty byte array. - # 4. When the path asked for is being monitored, but doesn't exist for that service. - def get_value(self, serviceName, objectPath, default_value=None): - service = self.servicesByName.get(serviceName, None) - if service is None: - return default_value - - value = service.paths.get(objectPath, None) - if value is None or value.value is None: - return default_value - - return value.value - - # returns if a dbus exists now, by doing a blocking dbus call. - # Typically seen will be sufficient and doesn't need access to the dbus. - def exists(self, serviceName, objectPath): - try: - self.dbusConn.call_blocking(serviceName, objectPath, None, 'GetValue', '', []) - return True - except dbus.exceptions.DBusException as e: - return False - - # Returns if there ever was a successful GetValue or valueChanged event. - # Unlike get_value this return True also if the actual value is invalid. - # - # Note: the path might no longer exists anymore, but that doesn't happen in - # practice. If a service really wants to reconfigure itself typically it should - # reconnect to the dbus which causes it to be rescanned and seen will be updated. - # If it is really needed to know if a path still exists, use exists. - def seen(self, serviceName, objectPath): - try: - return self.servicesByName[serviceName].seen(objectPath) - except KeyError: - return False - - # Sets the value for a certain servicename and path, returns the return value of the D-Bus SetValue - # method. If the underlying item does not exist (the service does not exist, or the objectPath was not - # registered) the function will return -1 - def set_value(self, serviceName, objectPath, value): - # Check if the D-Bus object referenced by serviceName and objectPath is registered. There is no - # necessity to do this, but it is in line with previous implementations which kept VeDbusItemImport - # objects for registers items only. - service = self.servicesByName.get(serviceName, None) - if service is None: - return -1 - if objectPath not in service.paths: - return -1 - # We do not catch D-Bus exceptions here, because the previous implementation did not do that either. - return self.dbusConn.call_blocking(serviceName, objectPath, - dbus_interface='com.victronenergy.BusItem', - method='SetValue', signature=None, - args=[wrap_dbus_value(value)]) - - # Similar to set_value, but operates asynchronously - def set_value_async(self, serviceName, objectPath, value, - reply_handler=None, error_handler=None): - service = self.servicesByName.get(serviceName, None) - if service is not None: - if objectPath in service.paths: - self.dbusConn.call_async(serviceName, objectPath, - dbus_interface='com.victronenergy.BusItem', - method='SetValue', signature=None, - args=[wrap_dbus_value(value)], - reply_handler=reply_handler, error_handler=error_handler) - return - - if error_handler is not None: - error_handler(TypeError('Service or path not found, ' - 'service=%s, path=%s' % (serviceName, objectPath))) - - # returns a dictionary, keys are the servicenames, value the instances - # optionally use the classfilter to get only a certain type of services, for - # example com.victronenergy.battery. - def get_service_list(self, classfilter=None): - if classfilter is None: - return { servicename: service.deviceInstance \ - for servicename, service in self.servicesByName.items() } - - if classfilter not in self.servicesByClass: - return {} - - return { service.name: service.deviceInstance \ - for service in self.servicesByClass[classfilter] } - - def get_device_instance(self, serviceName): - return self.servicesByName[serviceName].deviceInstance - - def track_value(self, serviceName, objectPath, callback, *args, **kwargs): - """ A DbusMonitor can watch specific service/path combos for changes - so that it is not fully reliant on the global handler_value_changes - in this class. Additional watches are deleted automatically when - the service disappears from dbus. """ - cb = partial(callback, *args, **kwargs) - - def root_tracker(items): - # Check if objectPath in dict - try: - v = items[objectPath] - _v = unwrap_dbus_value(v['Value']) - except (KeyError, TypeError): - return # not in this dict - - try: - t = v['Text'] - except KeyError: - cb({'Value': _v }) - else: - cb({'Value': _v, 'Text': t}) - - # Track changes on the path, and also on root - self.serviceWatches[serviceName].extend(( - self.dbusConn.add_signal_receiver(cb, - dbus_interface='com.victronenergy.BusItem', - signal_name='PropertiesChanged', - path=objectPath, bus_name=serviceName), - self.dbusConn.add_signal_receiver(root_tracker, - dbus_interface='com.victronenergy.BusItem', - signal_name='ItemsChanged', - path="/", bus_name=serviceName), - )) - - -# ====== ALL CODE BELOW THIS LINE IS PURELY FOR DEVELOPING THIS CLASS ====== - -# Example function that can be used as a starting point to use this code -def value_changed_on_dbus(dbusServiceName, dbusPath, dict, changes, deviceInstance): - logger.debug("0 ----------------") - logger.debug("1 %s%s changed" % (dbusServiceName, dbusPath)) - logger.debug("2 vrm dict : %s" % dict) - logger.debug("3 changes-text: %s" % changes['Text']) - logger.debug("4 changes-value: %s" % changes['Value']) - logger.debug("5 deviceInstance: %s" % deviceInstance) - logger.debug("6 - end") - - -def nameownerchange(a, b): - # used to find memory leaks in dbusmonitor and VeDbusItemImport - import gc - gc.collect() - objects = gc.get_objects() - print (len([o for o in objects if type(o).__name__ == 'VeDbusItemImport'])) - print (len([o for o in objects if type(o).__name__ == 'SignalMatch'])) - print (len(objects)) - - -def print_values(dbusmonitor): - a = dbusmonitor.get_value('wrongservice', '/DbusInvalid', default_value=1000) - b = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/NotInTheMonitorList', default_value=1000) - c = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/DbusInvalid', default_value=1000) - d = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/NonExistingButMonitored', default_value=1000) - - print ("All should be 1000: Wrong Service: %s, NotInTheMonitorList: %s, DbusInvalid: %s, NonExistingButMonitored: %s" % (a, b, c, d)) - return True - -# We have a mainloop, but that is just for developing this code. Normally above class & code is used from -# some other class, such as vrmLogger or the pubsub Implementation. -def main(): - # Init logging - logging.basicConfig(level=logging.DEBUG) - logger.info(__file__ + " is starting up") - - # Have a mainloop, so we can send/receive asynchronous calls to and from dbus - DBusGMainLoop(set_as_default=True) - - import os - import sys - sys.path.insert(1, os.path.join(os.path.dirname(__file__), '../../')) - - dummy = {'code': None, 'whenToLog': 'configChange', 'accessLevel': None} - monitorlist = {'com.victronenergy.dummyservice': { - '/Connected': dummy, - '/ProductName': dummy, - '/Mgmt/Connection': dummy, - '/Dc/0/Voltage': dummy, - '/Dc/0/Current': dummy, - '/Dc/0/Temperature': dummy, - '/Load/I': dummy, - '/FirmwareVersion': dummy, - '/DbusInvalid': dummy, - '/NonExistingButMonitored': dummy}} - - d = DbusMonitor(monitorlist, value_changed_on_dbus, - deviceAddedCallback=nameownerchange, deviceRemovedCallback=nameownerchange) - - GLib.timeout_add(1000, print_values, d) - - # Start and run the mainloop - logger.info("Starting mainloop, responding on only events") - mainloop = GLib.MainLoop() - mainloop.run() - -if __name__ == "__main__": - main() diff --git a/velib_python/v3.40~37/oldestVersion b/velib_python/v3.40~37/oldestVersion deleted file mode 100644 index 3f6c1a0..0000000 --- a/velib_python/v3.40~37/oldestVersion +++ /dev/null @@ -1 +0,0 @@ -v3.00~32 diff --git a/velib_python/v3.40~37/settingsdevice.py b/velib_python/v3.40~37/settingsdevice.py deleted file mode 100644 index a207e8b..0000000 --- a/velib_python/v3.40~37/settingsdevice.py +++ /dev/null @@ -1,118 +0,0 @@ -import dbus -import logging -import time -from functools import partial - -# Local imports -from vedbus import VeDbusItemImport - -## Indexes for the setting dictonary. -PATH = 0 -VALUE = 1 -MINIMUM = 2 -MAXIMUM = 3 -SILENT = 4 - -## The Settings Device class. -# Used by python programs, such as the vrm-logger, to read and write settings they -# need to store on disk. And since these settings might be changed from a different -# source, such as the GUI, the program can pass an eventCallback that will be called -# as soon as some setting is changed. -# -# The settings are stored in flash via the com.victronenergy.settings service on dbus. -# See https://github.com/victronenergy/localsettings for more info. -# -# If there are settings in de supportSettings list which are not yet on the dbus, -# and therefore not yet in the xml file, they will be added through the dbus-addSetting -# interface of com.victronenergy.settings. -class SettingsDevice(object): - ## The constructor processes the tree of dbus-items. - # @param bus the system-dbus object - # @param name the dbus-service-name of the settings dbus service, 'com.victronenergy.settings' - # @param supportedSettings dictionary with all setting-names, and their defaultvalue, min, max and whether - # the setting is silent. The 'silent' entry is optional. If set to true, no changes in the setting will - # be logged by localsettings. - # @param eventCallback function that will be called on changes on any of these settings - # @param timeout Maximum interval to wait for localsettings. An exception is thrown at the end of the - # interval if the localsettings D-Bus service has not appeared yet. - def __init__(self, bus, supportedSettings, eventCallback, name='com.victronenergy.settings', timeout=0): - logging.debug("===== Settings device init starting... =====") - self._bus = bus - self._dbus_name = name - self._eventCallback = eventCallback - self._values = {} # stored the values, used to pass the old value along on a setting change - self._settings = {} - - count = 0 - while True: - if 'com.victronenergy.settings' in self._bus.list_names(): - break - if count == timeout: - raise Exception("The settings service com.victronenergy.settings does not exist!") - count += 1 - logging.info('waiting for settings') - time.sleep(1) - - # Add the items. - self.addSettings(supportedSettings) - - logging.debug("===== Settings device init finished =====") - - def addSettings(self, settings): - for setting, options in settings.items(): - silent = len(options) > SILENT and options[SILENT] - busitem = self.addSetting(options[PATH], options[VALUE], - options[MINIMUM], options[MAXIMUM], silent, callback=partial(self.handleChangedSetting, setting)) - self._settings[setting] = busitem - self._values[setting] = busitem.get_value() - - def addSetting(self, path, value, _min, _max, silent=False, callback=None): - busitem = VeDbusItemImport(self._bus, self._dbus_name, path, callback) - if busitem.exists and (value, _min, _max, silent) == busitem._proxy.GetAttributes(): - logging.debug("Setting %s found" % path) - else: - logging.info("Setting %s does not exist yet or must be adjusted" % path) - - # Prepare to add the setting. Most dbus types extend the python - # type so it is only necessary to additionally test for Int64. - if isinstance(value, (int, dbus.Int64)): - itemType = 'i' - elif isinstance(value, float): - itemType = 'f' - else: - itemType = 's' - - # Add the setting - # TODO, make an object that inherits VeDbusItemImport, and complete the D-Bus settingsitem interface - settings_item = VeDbusItemImport(self._bus, self._dbus_name, '/Settings', createsignal=False) - setting_path = path.replace('/Settings/', '', 1) - if silent: - settings_item._proxy.AddSilentSetting('', setting_path, value, itemType, _min, _max) - else: - settings_item._proxy.AddSetting('', setting_path, value, itemType, _min, _max) - - busitem = VeDbusItemImport(self._bus, self._dbus_name, path, callback) - - return busitem - - def handleChangedSetting(self, setting, servicename, path, changes): - oldvalue = self._values[setting] if setting in self._values else None - self._values[setting] = changes['Value'] - - if self._eventCallback is None: - return - - self._eventCallback(setting, oldvalue, changes['Value']) - - def setDefault(self, path): - item = VeDbusItemImport(self._bus, self._dbus_name, path, createsignal=False) - item.set_default() - - def __getitem__(self, setting): - return self._settings[setting].get_value() - - def __setitem__(self, setting, newvalue): - result = self._settings[setting].set_value(newvalue) - if result != 0: - # Trying to make some false change to our own settings? How dumb! - assert False diff --git a/velib_python/v3.40~37/ve_utils.py b/velib_python/v3.40~37/ve_utils.py deleted file mode 100644 index 63a915b..0000000 --- a/velib_python/v3.40~37/ve_utils.py +++ /dev/null @@ -1,262 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -import sys -from traceback import print_exc -from os import _exit as os_exit -from os import statvfs -from subprocess import check_output, CalledProcessError -import logging -import dbus -logger = logging.getLogger(__name__) - -VEDBUS_INVALID = dbus.Array([], signature=dbus.Signature('i'), variant_level=1) - -class NoVrmPortalIdError(Exception): - pass - -# Use this function to make sure the code quits on an unexpected exception. Make sure to use it -# when using GLib.idle_add and also GLib.timeout_add. -# Without this, the code will just keep running, since GLib does not stop the mainloop on an -# exception. -# Example: GLib.idle_add(exit_on_error, myfunc, arg1, arg2) -def exit_on_error(func, *args, **kwargs): - try: - return func(*args, **kwargs) - except: - try: - print ('exit_on_error: there was an exception. Printing stacktrace will be tried and then exit') - print_exc() - except: - pass - - # sys.exit() is not used, since that throws an exception, which does not lead to a program - # halt when used in a dbus callback, see connection.py in the Python/Dbus libraries, line 230. - os_exit(1) - - -__vrm_portal_id = None -def get_vrm_portal_id(): - # The original definition of the VRM Portal ID is that it is the mac - # address of the onboard- ethernet port (eth0), stripped from its colons - # (:) and lower case. This may however differ between platforms. On Venus - # the task is therefore deferred to /sbin/get-unique-id so that a - # platform specific method can be easily defined. - # - # If /sbin/get-unique-id does not exist, then use the ethernet address - # of eth0. This also handles the case where velib_python is used as a - # package install on a Raspberry Pi. - # - # On a Linux host where the network interface may not be eth0, you can set - # the VRM_IFACE environment variable to the correct name. - - global __vrm_portal_id - - if __vrm_portal_id: - return __vrm_portal_id - - portal_id = None - - # First try the method that works if we don't have a data partition. This - # will fail when the current user is not root. - try: - portal_id = check_output("/sbin/get-unique-id").decode("utf-8", "ignore").strip() - if not portal_id: - raise NoVrmPortalIdError("get-unique-id returned blank") - __vrm_portal_id = portal_id - return portal_id - except CalledProcessError: - # get-unique-id returned non-zero - raise NoVrmPortalIdError("get-unique-id returned non-zero") - except OSError: - # File doesn't exist, use fallback - pass - - # Fall back to getting our id using a syscall. Assume we are on linux. - # Allow the user to override what interface is used using an environment - # variable. - import fcntl, socket, struct, os - - iface = os.environ.get('VRM_IFACE', 'eth0').encode('ascii') - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - try: - info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', iface[:15])) - except IOError: - raise NoVrmPortalIdError("ioctl failed for eth0") - - __vrm_portal_id = info[18:24].hex() - return __vrm_portal_id - - -# See VE.Can registers - public.docx for definition of this conversion -def convert_vreg_version_to_readable(version): - def str_to_arr(x, length): - a = [] - for i in range(0, len(x), length): - a.append(x[i:i+length]) - return a - - x = "%x" % version - x = x.upper() - - if len(x) == 5 or len(x) == 3 or len(x) == 1: - x = '0' + x - - a = str_to_arr(x, 2); - - # remove the first 00 if there are three bytes and it is 00 - if len(a) == 3 and a[0] == '00': - a.remove(0); - - # if we have two or three bytes now, and the first character is a 0, remove it - if len(a) >= 2 and a[0][0:1] == '0': - a[0] = a[0][1]; - - result = '' - for item in a: - result += ('.' if result != '' else '') + item - - - result = 'v' + result - - return result - - -def get_free_space(path): - result = -1 - - try: - s = statvfs(path) - result = s.f_frsize * s.f_bavail # Number of free bytes that ordinary users - except Exception as ex: - logger.info("Error while retrieving free space for path %s: %s" % (path, ex)) - - return result - - -def _get_sysfs_machine_name(): - try: - with open('/sys/firmware/devicetree/base/model', 'r') as f: - return f.read().rstrip('\x00') - except IOError: - pass - - return None - -# Returns None if it cannot find a machine name. Otherwise returns the string -# containing the name -def get_machine_name(): - # First try calling the venus utility script - try: - return check_output("/usr/bin/product-name").strip().decode('UTF-8') - except (CalledProcessError, OSError): - pass - - # Fall back to sysfs - name = _get_sysfs_machine_name() - if name is not None: - return name - - # Fall back to venus build machine name - try: - with open('/etc/venus/machine', 'r', encoding='UTF-8') as f: - return f.read().strip() - except IOError: - pass - - return None - - -def get_product_id(): - """ Find the machine ID and return it. """ - - # First try calling the venus utility script - try: - return check_output("/usr/bin/product-id").strip().decode('UTF-8') - except (CalledProcessError, OSError): - pass - - # Fall back machine name mechanism - name = _get_sysfs_machine_name() - return { - 'Color Control GX': 'C001', - 'Venus GX': 'C002', - 'Octo GX': 'C006', - 'EasySolar-II': 'C007', - 'MultiPlus-II': 'C008', - 'Maxi GX': 'C009', - 'Cerbo GX': 'C00A' - }.get(name, 'C003') # C003 is Generic - - -# Returns False if it cannot open the file. Otherwise returns its rstripped contents -def read_file(path): - content = False - - try: - with open(path, 'r') as f: - content = f.read().rstrip() - except Exception as ex: - logger.debug("Error while reading %s: %s" % (path, ex)) - - return content - - -def wrap_dbus_value(value): - if value is None: - return VEDBUS_INVALID - if isinstance(value, float): - return dbus.Double(value, variant_level=1) - if isinstance(value, bool): - return dbus.Boolean(value, variant_level=1) - if isinstance(value, int): - try: - return dbus.Int32(value, variant_level=1) - except OverflowError: - return dbus.Int64(value, variant_level=1) - if isinstance(value, str): - return dbus.String(value, variant_level=1) - if isinstance(value, list): - if len(value) == 0: - # If the list is empty we cannot infer the type of the contents. So assume unsigned integer. - # A (signed) integer is dangerous, because an empty list of signed integers is used to encode - # an invalid value. - return dbus.Array([], signature=dbus.Signature('u'), variant_level=1) - return dbus.Array([wrap_dbus_value(x) for x in value], variant_level=1) - if isinstance(value, dict): - # Wrapping the keys of the dictionary causes D-Bus errors like: - # 'arguments to dbus_message_iter_open_container() were incorrect, - # assertion "(type == DBUS_TYPE_ARRAY && contained_signature && - # *contained_signature == DBUS_DICT_ENTRY_BEGIN_CHAR) || (contained_signature == NULL || - # _dbus_check_is_valid_signature (contained_signature))" failed in file ...' - return dbus.Dictionary({(k, wrap_dbus_value(v)) for k, v in value.items()}, variant_level=1) - return value - - -dbus_int_types = (dbus.Int32, dbus.UInt32, dbus.Byte, dbus.Int16, dbus.UInt16, dbus.UInt32, dbus.Int64, dbus.UInt64) - - -def unwrap_dbus_value(val): - """Converts D-Bus values back to the original type. For example if val is of type DBus.Double, - a float will be returned.""" - if isinstance(val, dbus_int_types): - return int(val) - if isinstance(val, dbus.Double): - return float(val) - if isinstance(val, dbus.Array): - v = [unwrap_dbus_value(x) for x in val] - return None if len(v) == 0 else v - if isinstance(val, (dbus.Signature, dbus.String)): - return str(val) - # Python has no byte type, so we convert to an integer. - if isinstance(val, dbus.Byte): - return int(val) - if isinstance(val, dbus.ByteArray): - return "".join([bytes(x) for x in val]) - if isinstance(val, (list, tuple)): - return [unwrap_dbus_value(x) for x in val] - if isinstance(val, (dbus.Dictionary, dict)): - # Do not unwrap the keys, see comment in wrap_dbus_value - return dict([(x, unwrap_dbus_value(y)) for x, y in val.items()]) - if isinstance(val, dbus.Boolean): - return bool(val) - return val diff --git a/velib_python/v3.40~37/vedbus.py b/velib_python/v3.40~37/vedbus.py deleted file mode 100644 index 8c101ea..0000000 --- a/velib_python/v3.40~37/vedbus.py +++ /dev/null @@ -1,611 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import dbus.service -import logging -import traceback -import os -import weakref -from collections import defaultdict -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 = {} - self._ratelimiters = [] - self._dbusname = None - - # 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['/'] = VeDbusRootExport(self._dbusconn, '/', self) - - logging.info("registered ourselves on D-Bus as %s" % servicename) - - # To force immediate deregistering of this dbus service and all its object paths, explicitly - # call __del__(). - def __del__(self): - for node in list(self._dbusnodes.values()): - node.__del__() - self._dbusnodes.clear() - for item in list(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, valuetype=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, valuetype=valuetype) - - 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] = VeDbusTreeExport(self._dbusconn, subPath, self) - 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) - - # 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 list(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 - - def __enter__(self): - l = ServiceContext(self) - self._ratelimiters.append(l) - return l - - def __exit__(self, *exc): - # pop off the top one and flush it. If with statements are nested - # then each exit flushes its own part. - if self._ratelimiters: - self._ratelimiters.pop().flush() - -class ServiceContext(object): - def __init__(self, parent): - self.parent = parent - self.changes = {} - - def __getitem__(self, path): - return self.parent[path] - - def __setitem__(self, path, newvalue): - c = self.parent._dbusobjects[path]._local_set_value(newvalue) - if c is not None: - self.changes[path] = c - - def flush(self): - if self.changes: - self.parent._dbusnodes['/'].ItemsChanged(self.changes) - -class TrackerDict(defaultdict): - """ Same as defaultdict, but passes the key to default_factory. """ - def __missing__(self, key): - self[key] = x = self.default_factory(key) - return x - -class VeDbusRootTracker(object): - """ This tracks the root of a dbus path and listens for PropertiesChanged - signals. When a signal arrives, parse it and unpack the key/value changes - into traditional events, then pass it to the original eventCallback - method. """ - def __init__(self, bus, serviceName): - self.importers = defaultdict(weakref.WeakSet) - self.serviceName = serviceName - self._match = bus.get_object(serviceName, '/', introspect=False).connect_to_signal( - "ItemsChanged", weak_functor(self._items_changed_handler)) - - def __del__(self): - self._match.remove() - self._match = None - - def add(self, i): - self.importers[i.path].add(i) - - def _items_changed_handler(self, items): - if not isinstance(items, dict): - return - - for path, changes in items.items(): - try: - v = changes['Value'] - except KeyError: - continue - - try: - t = changes['Text'] - except KeyError: - t = str(unwrap_dbus_value(v)) - - for i in self.importers.get(path, ()): - i._properties_changed_handler({'Value': v, 'Text': t}) - -""" -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): - def __new__(cls, bus, serviceName, path, eventCallback=None, createsignal=True): - instance = object.__new__(cls) - - # If signal tracking should be done, also add to root tracker - if createsignal: - if "_roots" not in cls.__dict__: - cls._roots = TrackerDict(lambda k: VeDbusRootTracker(bus, k)) - - return instance - - ## 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)) - self._roots[serviceName].add(self) - - # 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 is not 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 - - ## Resets the item to its default value - def set_default(self): - self._proxy.SetDefault() - self._refreshcachedvalue() - - ## 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, service): - dbus.service.Object.__init__(self, bus, objectPath) - self._service = service - 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] - - def _get_value_handler(self, path, get_text=False): - logging.debug("_get_value_handler called for %s" % path) - r = {} - px = path - if not px.endswith('/'): - px += '/' - for p, item in self._service._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 - - @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 VeDbusRootExport(VeDbusTreeExport): - @dbus.service.signal('com.victronenergy.BusItem', signature='a{sa{sv}}') - def ItemsChanged(self, changes): - pass - - @dbus.service.method('com.victronenergy.BusItem', out_signature='a{sa{sv}}') - def GetItems(self): - return { - path: { - 'Value': wrap_dbus_value(item.local_get_value()), - 'Text': item.GetText() } - for path, item in self._service._dbusobjects.items() - } - - -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, - valuetype=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 - self._type = valuetype - - # 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.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): - changes = self._local_set_value(newvalue) - if changes is not None: - self.PropertiesChanged(changes) - - def _local_set_value(self, newvalue): - if self._value == newvalue: - return None - - self._value = newvalue - return { - 'Value': wrap_dbus_value(newvalue), - 'Text': self.GetText() - } - - 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 value type is enforced, cast it. If the type can be coerced - # python will do it for us. This allows ints to become floats, - # or bools to become ints. Additionally also allow None, so that - # a path may be invalidated. - if self._type is not None and newvalue is not None: - try: - newvalue = self._type(newvalue) - except (ValueError, TypeError): - return 1 # NOT OK - - 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) diff --git a/velib_python/v3.40~38/dbusmonitor.py b/velib_python/v3.40~38/dbusmonitor.py deleted file mode 100644 index fd25700..0000000 --- a/velib_python/v3.40~38/dbusmonitor.py +++ /dev/null @@ -1,587 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -## @package dbus_vrm -# This code takes care of the D-Bus interface (not all of below is implemented yet): -# - on startup it scans the dbus for services we know. For each known service found, it searches for -# objects/paths we know. Everything we find is stored in items{}, and an event is registered: if a -# value changes weĺl be notified and can pass that on to our owner. For example the vrmLogger. -# we know. -# - after startup, it continues to monitor the dbus: -# 1) when services are added we do the same check on that -# 2) when services are removed, we remove any items that we had that referred to that service -# 3) if an existing services adds paths we update ourselves as well: on init, we make a -# VeDbusItemImport for a non-, or not yet existing objectpaths as well1 -# -# Code is used by the vrmLogger, and also the pubsub code. Both are other modules in the dbus_vrm repo. - -from dbus.mainloop.glib import DBusGMainLoop -from gi.repository import GLib -import dbus -import dbus.service -import inspect -import logging -import argparse -import pprint -import traceback -import os -from collections import defaultdict -from functools import partial - -# our own packages -from ve_utils import exit_on_error, wrap_dbus_value, unwrap_dbus_value, add_name_owner_changed_receiver -notfound = object() # For lookups where None is a valid result - -logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) -class SystemBus(dbus.bus.BusConnection): - def __new__(cls): - return dbus.bus.BusConnection.__new__(cls, dbus.bus.BusConnection.TYPE_SYSTEM) - -class SessionBus(dbus.bus.BusConnection): - def __new__(cls): - return dbus.bus.BusConnection.__new__(cls, dbus.bus.BusConnection.TYPE_SESSION) - -class MonitoredValue(object): - def __init__(self, value, text, options): - super(MonitoredValue, self).__init__() - self.value = value - self.text = text - self.options = options - - # For legacy code, allow treating this as a tuple/list - def __iter__(self): - return iter((self.value, self.text, self.options)) - -class Service(object): - def __init__(self, id, serviceName, deviceInstance): - super(Service, self).__init__() - self.id = id - self.name = serviceName - self.paths = {} - self._seen = set() - self.deviceInstance = deviceInstance - - # For legacy code, attributes can still be accessed as if keys from a - # dictionary. - def __setitem__(self, key, value): - self.__dict__[key] = value - def __getitem__(self, key): - return self.__dict__[key] - - def set_seen(self, path): - self._seen.add(path) - - def seen(self, path): - return path in self._seen - - @property - def service_class(self): - return '.'.join(self.name.split('.')[:3]) - -class DbusMonitor(object): - ## Constructor - def __init__(self, dbusTree, valueChangedCallback=None, deviceAddedCallback=None, - deviceRemovedCallback=None, namespace="com.victronenergy", ignoreServices=[]): - # valueChangedCallback is the callback that we call when something has changed. - # def value_changed_on_dbus(dbusServiceName, dbusPath, options, changes, deviceInstance): - # in which changes is a tuple with GetText() and GetValue() - self.valueChangedCallback = valueChangedCallback - self.deviceAddedCallback = deviceAddedCallback - self.deviceRemovedCallback = deviceRemovedCallback - self.dbusTree = dbusTree - self.ignoreServices = ignoreServices - - # Lists all tracked services. Stores name, id, device instance, value per path, and whenToLog info - # indexed by service name (eg. com.victronenergy.settings). - self.servicesByName = {} - - # Same values as self.servicesByName, but indexed by service id (eg. :1.30) - self.servicesById = {} - - # Keep track of services by class to speed up calls to get_service_list - self.servicesByClass = defaultdict(list) - - # Keep track of any additional watches placed on items - self.serviceWatches = defaultdict(list) - - # For a PC, connect to the SessionBus - # For a CCGX, connect to the SystemBus - self.dbusConn = SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else SystemBus() - - # subscribe to NameOwnerChange for bus connect / disconnect events. - # NOTE: this is on a different bus then the one above! - standardBus = (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ \ - else dbus.SystemBus()) - - add_name_owner_changed_receiver(standardBus, self.dbus_name_owner_changed) - - # Subscribe to PropertiesChanged for all services - self.dbusConn.add_signal_receiver(self.handler_value_changes, - dbus_interface='com.victronenergy.BusItem', - signal_name='PropertiesChanged', path_keyword='path', - sender_keyword='senderId') - - # Subscribe to ItemsChanged for all services - self.dbusConn.add_signal_receiver(self.handler_item_changes, - dbus_interface='com.victronenergy.BusItem', - signal_name='ItemsChanged', path='/', - sender_keyword='senderId') - - logger.info('===== Search on dbus for services that we will monitor starting... =====') - serviceNames = self.dbusConn.list_names() - for serviceName in serviceNames: - self.scan_dbus_service(serviceName) - - logger.info('===== Search on dbus for services that we will monitor finished =====') - - @staticmethod - def make_service(serviceId, serviceName, deviceInstance): - """ Override this to use a different kind of service object. """ - return Service(serviceId, serviceName, deviceInstance) - - def make_monitor(self, service, path, value, text, options): - """ Override this to do more things with monitoring. """ - return MonitoredValue(unwrap_dbus_value(value), unwrap_dbus_value(text), options) - - def dbus_name_owner_changed(self, name, oldowner, newowner): - if not name.startswith("com.victronenergy."): - return - - #decouple, and process in main loop - GLib.idle_add(exit_on_error, self._process_name_owner_changed, name, oldowner, newowner) - - def _process_name_owner_changed(self, name, oldowner, newowner): - if newowner != '': - # so we found some new service. Check if we can do something with it. - newdeviceadded = self.scan_dbus_service(name) - if newdeviceadded and self.deviceAddedCallback is not None: - self.deviceAddedCallback(name, self.get_device_instance(name)) - - elif name in self.servicesByName: - # it disappeared, we need to remove it. - logger.info("%s disappeared from the dbus. Removing it from our lists" % name) - service = self.servicesByName[name] - del self.servicesById[service.id] - del self.servicesByName[name] - for watch in self.serviceWatches[name]: - watch.remove() - del self.serviceWatches[name] - self.servicesByClass[service.service_class].remove(service) - if self.deviceRemovedCallback is not None: - self.deviceRemovedCallback(name, service.deviceInstance) - - def scan_dbus_service(self, serviceName): - try: - return self.scan_dbus_service_inner(serviceName) - except: - logger.error("Ignoring %s because of error while scanning:" % (serviceName)) - traceback.print_exc() - return False - - # Errors 'org.freedesktop.DBus.Error.ServiceUnknown' and - # 'org.freedesktop.DBus.Error.Disconnected' seem to happen when the service - # disappears while its being scanned. Which might happen, but is not really - # normal either, so letting them go into the logs. - - # Scans the given dbus service to see if it contains anything interesting for us. If it does, add - # it to our list of monitored D-Bus services. - def scan_dbus_service_inner(self, serviceName): - - # make it a normal string instead of dbus string - serviceName = str(serviceName) - - if (len(self.ignoreServices) != 0 and any(serviceName.startswith(x) for x in self.ignoreServices)): - logger.debug("Ignoring service %s" % serviceName) - return False - - paths = self.dbusTree.get('.'.join(serviceName.split('.')[0:3]), None) - if paths is None: - logger.debug("Ignoring service %s, not in the tree" % serviceName) - return False - - logger.info("Found: %s, scanning and storing items" % serviceName) - serviceId = self.dbusConn.get_name_owner(serviceName) - - # we should never be notified to add a D-Bus service that we already have. If this assertion - # raises, check process_name_owner_changed, and D-Bus workings. - assert serviceName not in self.servicesByName - assert serviceId not in self.servicesById - - # Try to fetch everything with a GetItems, then fall back to older - # methods if that fails - try: - values = self.dbusConn.call_blocking(serviceName, '/', None, 'GetItems', '', []) - except dbus.exceptions.DBusException: - logger.info("GetItems failed, trying legacy methods") - else: - return self.scan_dbus_service_getitems_done(serviceName, serviceId, values) - - if serviceName == 'com.victronenergy.settings': - di = 0 - elif serviceName.startswith('com.victronenergy.vecan.'): - di = 0 - else: - try: - di = self.dbusConn.call_blocking(serviceName, - '/DeviceInstance', None, 'GetValue', '', []) - except dbus.exceptions.DBusException: - logger.info(" %s was skipped because it has no device instance" % serviceName) - return False # Skip it - else: - di = int(di) - - logger.info(" %s has device instance %s" % (serviceName, di)) - service = self.make_service(serviceId, serviceName, di) - - # Let's try to fetch everything in one go - values = {} - texts = {} - try: - values.update(self.dbusConn.call_blocking(serviceName, '/', None, 'GetValue', '', [])) - texts.update(self.dbusConn.call_blocking(serviceName, '/', None, 'GetText', '', [])) - except: - pass - - for path, options in paths.items(): - # path will be the D-Bus path: '/Ac/ActiveIn/L1/V' - # options will be a dictionary: {'code': 'V', 'whenToLog': 'onIntervalAlways'} - - # Try to obtain the value we want from our bulk fetch. If we - # cannot find it there, do an individual query. - value = values.get(path[1:], notfound) - if value != notfound: - service.set_seen(path) - text = texts.get(path[1:], notfound) - if value is notfound or text is notfound: - try: - value = self.dbusConn.call_blocking(serviceName, path, None, 'GetValue', '', []) - service.set_seen(path) - text = self.dbusConn.call_blocking(serviceName, path, None, 'GetText', '', []) - except dbus.exceptions.DBusException as e: - if e.get_dbus_name() in ( - 'org.freedesktop.DBus.Error.ServiceUnknown', - 'org.freedesktop.DBus.Error.Disconnected'): - raise # This exception will be handled below - - # TODO org.freedesktop.DBus.Error.UnknownMethod really - # shouldn't happen but sometimes does. - logger.debug("%s %s does not exist (yet)" % (serviceName, path)) - value = None - text = None - - service.paths[path] = self.make_monitor(service, path, unwrap_dbus_value(value), unwrap_dbus_value(text), options) - - - logger.debug("Finished scanning and storing items for %s" % serviceName) - - # Adjust self at the end of the scan, so we don't have an incomplete set of - # data if an exception occurs during the scan. - self.servicesByName[serviceName] = service - self.servicesById[serviceId] = service - self.servicesByClass[service.service_class].append(service) - - return True - - def scan_dbus_service_getitems_done(self, serviceName, serviceId, values): - # Keeping these exceptions for legacy reasons - if serviceName == 'com.victronenergy.settings': - di = 0 - elif serviceName.startswith('com.victronenergy.vecan.'): - di = 0 - else: - try: - di = values['/DeviceInstance']['Value'] - except KeyError: - logger.info(" %s was skipped because it has no device instance" % serviceName) - return False - else: - di = int(di) - - logger.info(" %s has device instance %s" % (serviceName, di)) - service = self.make_service(serviceId, serviceName, di) - - paths = self.dbusTree.get('.'.join(serviceName.split('.')[0:3]), {}) - for path, options in paths.items(): - item = values.get(path, notfound) - if item is notfound: - service.paths[path] = self.make_monitor(service, path, None, None, options) - else: - service.set_seen(path) - value = item.get('Value', None) - text = item.get('Text', None) - service.paths[path] = self.make_monitor(service, path, unwrap_dbus_value(value), unwrap_dbus_value(text), options) - - self.servicesByName[serviceName] = service - self.servicesById[serviceId] = service - self.servicesByClass[service.service_class].append(service) - return True - - def handler_item_changes(self, items, senderId): - if not isinstance(items, dict): - return - - try: - service = self.servicesById[senderId] - except KeyError: - # senderId isn't there, which means it hasn't been scanned yet. - return - - for path, changes in items.items(): - try: - v = unwrap_dbus_value(changes['Value']) - except (KeyError, TypeError): - continue - - try: - t = changes['Text'] - except KeyError: - t = str(v) - self._handler_value_changes(service, path, v, t) - - def handler_value_changes(self, changes, path, senderId): - # If this properyChange does not involve a value, our work is done. - if 'Value' not in changes: - return - - try: - service = self.servicesById[senderId] - except KeyError: - # senderId isn't there, which means it hasn't been scanned yet. - return - - v = unwrap_dbus_value(changes['Value']) - # Some services don't send Text with their PropertiesChanged events. - try: - t = changes['Text'] - except KeyError: - t = str(v) - self._handler_value_changes(service, path, v, t) - - def _handler_value_changes(self, service, path, value, text): - try: - a = service.paths[path] - except KeyError: - # path isn't there, which means it hasn't been scanned yet. - return - - service.set_seen(path) - - # First update our store to the new value - if a.value == value: - return - - a.value = value - a.text = text - - # And do the rest of the processing in on the mainloop - if self.valueChangedCallback is not None: - GLib.idle_add(exit_on_error, self._execute_value_changes, service.name, path, { - 'Value': value, 'Text': text}, a.options) - - def _execute_value_changes(self, serviceName, objectPath, changes, options): - # double check that the service still exists, as it might have - # disappeared between scheduling-for and executing this function. - if serviceName not in self.servicesByName: - return - - self.valueChangedCallback(serviceName, objectPath, - options, changes, self.get_device_instance(serviceName)) - - # Gets the value for a certain servicename and path - # The default_value is returned when: - # 1. When the service doesn't exist. - # 2. When the path asked for isn't being monitored. - # 3. When the path exists, but has dbus-invalid, ie an empty byte array. - # 4. When the path asked for is being monitored, but doesn't exist for that service. - def get_value(self, serviceName, objectPath, default_value=None): - service = self.servicesByName.get(serviceName, None) - if service is None: - return default_value - - value = service.paths.get(objectPath, None) - if value is None or value.value is None: - return default_value - - return value.value - - # returns if a dbus exists now, by doing a blocking dbus call. - # Typically seen will be sufficient and doesn't need access to the dbus. - def exists(self, serviceName, objectPath): - try: - self.dbusConn.call_blocking(serviceName, objectPath, None, 'GetValue', '', []) - return True - except dbus.exceptions.DBusException as e: - return False - - # Returns if there ever was a successful GetValue or valueChanged event. - # Unlike get_value this return True also if the actual value is invalid. - # - # Note: the path might no longer exists anymore, but that doesn't happen in - # practice. If a service really wants to reconfigure itself typically it should - # reconnect to the dbus which causes it to be rescanned and seen will be updated. - # If it is really needed to know if a path still exists, use exists. - def seen(self, serviceName, objectPath): - try: - return self.servicesByName[serviceName].seen(objectPath) - except KeyError: - return False - - # Sets the value for a certain servicename and path, returns the return value of the D-Bus SetValue - # method. If the underlying item does not exist (the service does not exist, or the objectPath was not - # registered) the function will return -1 - def set_value(self, serviceName, objectPath, value): - # Check if the D-Bus object referenced by serviceName and objectPath is registered. There is no - # necessity to do this, but it is in line with previous implementations which kept VeDbusItemImport - # objects for registers items only. - service = self.servicesByName.get(serviceName, None) - if service is None: - return -1 - if objectPath not in service.paths: - return -1 - # We do not catch D-Bus exceptions here, because the previous implementation did not do that either. - return self.dbusConn.call_blocking(serviceName, objectPath, - dbus_interface='com.victronenergy.BusItem', - method='SetValue', signature=None, - args=[wrap_dbus_value(value)]) - - # Similar to set_value, but operates asynchronously - def set_value_async(self, serviceName, objectPath, value, - reply_handler=None, error_handler=None): - service = self.servicesByName.get(serviceName, None) - if service is not None: - if objectPath in service.paths: - self.dbusConn.call_async(serviceName, objectPath, - dbus_interface='com.victronenergy.BusItem', - method='SetValue', signature=None, - args=[wrap_dbus_value(value)], - reply_handler=reply_handler, error_handler=error_handler) - return - - if error_handler is not None: - error_handler(TypeError('Service or path not found, ' - 'service=%s, path=%s' % (serviceName, objectPath))) - - # returns a dictionary, keys are the servicenames, value the instances - # optionally use the classfilter to get only a certain type of services, for - # example com.victronenergy.battery. - def get_service_list(self, classfilter=None): - if classfilter is None: - return { servicename: service.deviceInstance \ - for servicename, service in self.servicesByName.items() } - - if classfilter not in self.servicesByClass: - return {} - - return { service.name: service.deviceInstance \ - for service in self.servicesByClass[classfilter] } - - def get_device_instance(self, serviceName): - return self.servicesByName[serviceName].deviceInstance - - def track_value(self, serviceName, objectPath, callback, *args, **kwargs): - """ A DbusMonitor can watch specific service/path combos for changes - so that it is not fully reliant on the global handler_value_changes - in this class. Additional watches are deleted automatically when - the service disappears from dbus. """ - cb = partial(callback, *args, **kwargs) - - def root_tracker(items): - # Check if objectPath in dict - try: - v = items[objectPath] - _v = unwrap_dbus_value(v['Value']) - except (KeyError, TypeError): - return # not in this dict - - try: - t = v['Text'] - except KeyError: - cb({'Value': _v }) - else: - cb({'Value': _v, 'Text': t}) - - # Track changes on the path, and also on root - self.serviceWatches[serviceName].extend(( - self.dbusConn.add_signal_receiver(cb, - dbus_interface='com.victronenergy.BusItem', - signal_name='PropertiesChanged', - path=objectPath, bus_name=serviceName), - self.dbusConn.add_signal_receiver(root_tracker, - dbus_interface='com.victronenergy.BusItem', - signal_name='ItemsChanged', - path="/", bus_name=serviceName), - )) - - -# ====== ALL CODE BELOW THIS LINE IS PURELY FOR DEVELOPING THIS CLASS ====== - -# Example function that can be used as a starting point to use this code -def value_changed_on_dbus(dbusServiceName, dbusPath, dict, changes, deviceInstance): - logger.debug("0 ----------------") - logger.debug("1 %s%s changed" % (dbusServiceName, dbusPath)) - logger.debug("2 vrm dict : %s" % dict) - logger.debug("3 changes-text: %s" % changes['Text']) - logger.debug("4 changes-value: %s" % changes['Value']) - logger.debug("5 deviceInstance: %s" % deviceInstance) - logger.debug("6 - end") - - -def nameownerchange(a, b): - # used to find memory leaks in dbusmonitor and VeDbusItemImport - import gc - gc.collect() - objects = gc.get_objects() - print (len([o for o in objects if type(o).__name__ == 'VeDbusItemImport'])) - print (len([o for o in objects if type(o).__name__ == 'SignalMatch'])) - print (len(objects)) - - -def print_values(dbusmonitor): - a = dbusmonitor.get_value('wrongservice', '/DbusInvalid', default_value=1000) - b = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/NotInTheMonitorList', default_value=1000) - c = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/DbusInvalid', default_value=1000) - d = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/NonExistingButMonitored', default_value=1000) - - print ("All should be 1000: Wrong Service: %s, NotInTheMonitorList: %s, DbusInvalid: %s, NonExistingButMonitored: %s" % (a, b, c, d)) - return True - -# We have a mainloop, but that is just for developing this code. Normally above class & code is used from -# some other class, such as vrmLogger or the pubsub Implementation. -def main(): - # Init logging - logging.basicConfig(level=logging.DEBUG) - logger.info(__file__ + " is starting up") - - # Have a mainloop, so we can send/receive asynchronous calls to and from dbus - DBusGMainLoop(set_as_default=True) - - import os - import sys - sys.path.insert(1, os.path.join(os.path.dirname(__file__), '../../')) - - dummy = {'code': None, 'whenToLog': 'configChange', 'accessLevel': None} - monitorlist = {'com.victronenergy.dummyservice': { - '/Connected': dummy, - '/ProductName': dummy, - '/Mgmt/Connection': dummy, - '/Dc/0/Voltage': dummy, - '/Dc/0/Current': dummy, - '/Dc/0/Temperature': dummy, - '/Load/I': dummy, - '/FirmwareVersion': dummy, - '/DbusInvalid': dummy, - '/NonExistingButMonitored': dummy}} - - d = DbusMonitor(monitorlist, value_changed_on_dbus, - deviceAddedCallback=nameownerchange, deviceRemovedCallback=nameownerchange) - - GLib.timeout_add(1000, print_values, d) - - # Start and run the mainloop - logger.info("Starting mainloop, responding on only events") - mainloop = GLib.MainLoop() - mainloop.run() - -if __name__ == "__main__": - main() diff --git a/velib_python/v3.40~38/oldestVersion b/velib_python/v3.40~38/oldestVersion deleted file mode 100644 index 8aa0dc8..0000000 --- a/velib_python/v3.40~38/oldestVersion +++ /dev/null @@ -1 +0,0 @@ -v3.40~38 diff --git a/velib_python/v3.40~38/settingsdevice.py b/velib_python/v3.40~38/settingsdevice.py deleted file mode 100644 index a207e8b..0000000 --- a/velib_python/v3.40~38/settingsdevice.py +++ /dev/null @@ -1,118 +0,0 @@ -import dbus -import logging -import time -from functools import partial - -# Local imports -from vedbus import VeDbusItemImport - -## Indexes for the setting dictonary. -PATH = 0 -VALUE = 1 -MINIMUM = 2 -MAXIMUM = 3 -SILENT = 4 - -## The Settings Device class. -# Used by python programs, such as the vrm-logger, to read and write settings they -# need to store on disk. And since these settings might be changed from a different -# source, such as the GUI, the program can pass an eventCallback that will be called -# as soon as some setting is changed. -# -# The settings are stored in flash via the com.victronenergy.settings service on dbus. -# See https://github.com/victronenergy/localsettings for more info. -# -# If there are settings in de supportSettings list which are not yet on the dbus, -# and therefore not yet in the xml file, they will be added through the dbus-addSetting -# interface of com.victronenergy.settings. -class SettingsDevice(object): - ## The constructor processes the tree of dbus-items. - # @param bus the system-dbus object - # @param name the dbus-service-name of the settings dbus service, 'com.victronenergy.settings' - # @param supportedSettings dictionary with all setting-names, and their defaultvalue, min, max and whether - # the setting is silent. The 'silent' entry is optional. If set to true, no changes in the setting will - # be logged by localsettings. - # @param eventCallback function that will be called on changes on any of these settings - # @param timeout Maximum interval to wait for localsettings. An exception is thrown at the end of the - # interval if the localsettings D-Bus service has not appeared yet. - def __init__(self, bus, supportedSettings, eventCallback, name='com.victronenergy.settings', timeout=0): - logging.debug("===== Settings device init starting... =====") - self._bus = bus - self._dbus_name = name - self._eventCallback = eventCallback - self._values = {} # stored the values, used to pass the old value along on a setting change - self._settings = {} - - count = 0 - while True: - if 'com.victronenergy.settings' in self._bus.list_names(): - break - if count == timeout: - raise Exception("The settings service com.victronenergy.settings does not exist!") - count += 1 - logging.info('waiting for settings') - time.sleep(1) - - # Add the items. - self.addSettings(supportedSettings) - - logging.debug("===== Settings device init finished =====") - - def addSettings(self, settings): - for setting, options in settings.items(): - silent = len(options) > SILENT and options[SILENT] - busitem = self.addSetting(options[PATH], options[VALUE], - options[MINIMUM], options[MAXIMUM], silent, callback=partial(self.handleChangedSetting, setting)) - self._settings[setting] = busitem - self._values[setting] = busitem.get_value() - - def addSetting(self, path, value, _min, _max, silent=False, callback=None): - busitem = VeDbusItemImport(self._bus, self._dbus_name, path, callback) - if busitem.exists and (value, _min, _max, silent) == busitem._proxy.GetAttributes(): - logging.debug("Setting %s found" % path) - else: - logging.info("Setting %s does not exist yet or must be adjusted" % path) - - # Prepare to add the setting. Most dbus types extend the python - # type so it is only necessary to additionally test for Int64. - if isinstance(value, (int, dbus.Int64)): - itemType = 'i' - elif isinstance(value, float): - itemType = 'f' - else: - itemType = 's' - - # Add the setting - # TODO, make an object that inherits VeDbusItemImport, and complete the D-Bus settingsitem interface - settings_item = VeDbusItemImport(self._bus, self._dbus_name, '/Settings', createsignal=False) - setting_path = path.replace('/Settings/', '', 1) - if silent: - settings_item._proxy.AddSilentSetting('', setting_path, value, itemType, _min, _max) - else: - settings_item._proxy.AddSetting('', setting_path, value, itemType, _min, _max) - - busitem = VeDbusItemImport(self._bus, self._dbus_name, path, callback) - - return busitem - - def handleChangedSetting(self, setting, servicename, path, changes): - oldvalue = self._values[setting] if setting in self._values else None - self._values[setting] = changes['Value'] - - if self._eventCallback is None: - return - - self._eventCallback(setting, oldvalue, changes['Value']) - - def setDefault(self, path): - item = VeDbusItemImport(self._bus, self._dbus_name, path, createsignal=False) - item.set_default() - - def __getitem__(self, setting): - return self._settings[setting].get_value() - - def __setitem__(self, setting, newvalue): - result = self._settings[setting].set_value(newvalue) - if result != 0: - # Trying to make some false change to our own settings? How dumb! - assert False diff --git a/velib_python/v3.40~38/ve_utils.py b/velib_python/v3.40~38/ve_utils.py deleted file mode 100644 index f5a2f85..0000000 --- a/velib_python/v3.40~38/ve_utils.py +++ /dev/null @@ -1,276 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -import sys -from traceback import print_exc -from os import _exit as os_exit -from os import statvfs -from subprocess import check_output, CalledProcessError -import logging -import dbus -logger = logging.getLogger(__name__) - -VEDBUS_INVALID = dbus.Array([], signature=dbus.Signature('i'), variant_level=1) - -class NoVrmPortalIdError(Exception): - pass - -# Use this function to make sure the code quits on an unexpected exception. Make sure to use it -# when using GLib.idle_add and also GLib.timeout_add. -# Without this, the code will just keep running, since GLib does not stop the mainloop on an -# exception. -# Example: GLib.idle_add(exit_on_error, myfunc, arg1, arg2) -def exit_on_error(func, *args, **kwargs): - try: - return func(*args, **kwargs) - except: - try: - print ('exit_on_error: there was an exception. Printing stacktrace will be tried and then exit') - print_exc() - except: - pass - - # sys.exit() is not used, since that throws an exception, which does not lead to a program - # halt when used in a dbus callback, see connection.py in the Python/Dbus libraries, line 230. - os_exit(1) - - -__vrm_portal_id = None -def get_vrm_portal_id(): - # The original definition of the VRM Portal ID is that it is the mac - # address of the onboard- ethernet port (eth0), stripped from its colons - # (:) and lower case. This may however differ between platforms. On Venus - # the task is therefore deferred to /sbin/get-unique-id so that a - # platform specific method can be easily defined. - # - # If /sbin/get-unique-id does not exist, then use the ethernet address - # of eth0. This also handles the case where velib_python is used as a - # package install on a Raspberry Pi. - # - # On a Linux host where the network interface may not be eth0, you can set - # the VRM_IFACE environment variable to the correct name. - - global __vrm_portal_id - - if __vrm_portal_id: - return __vrm_portal_id - - portal_id = None - - # First try the method that works if we don't have a data partition. This - # will fail when the current user is not root. - try: - portal_id = check_output("/sbin/get-unique-id").decode("utf-8", "ignore").strip() - if not portal_id: - raise NoVrmPortalIdError("get-unique-id returned blank") - __vrm_portal_id = portal_id - return portal_id - except CalledProcessError: - # get-unique-id returned non-zero - raise NoVrmPortalIdError("get-unique-id returned non-zero") - except OSError: - # File doesn't exist, use fallback - pass - - # Fall back to getting our id using a syscall. Assume we are on linux. - # Allow the user to override what interface is used using an environment - # variable. - import fcntl, socket, struct, os - - iface = os.environ.get('VRM_IFACE', 'eth0').encode('ascii') - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - try: - info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', iface[:15])) - except IOError: - raise NoVrmPortalIdError("ioctl failed for eth0") - - __vrm_portal_id = info[18:24].hex() - return __vrm_portal_id - - -# See VE.Can registers - public.docx for definition of this conversion -def convert_vreg_version_to_readable(version): - def str_to_arr(x, length): - a = [] - for i in range(0, len(x), length): - a.append(x[i:i+length]) - return a - - x = "%x" % version - x = x.upper() - - if len(x) == 5 or len(x) == 3 or len(x) == 1: - x = '0' + x - - a = str_to_arr(x, 2); - - # remove the first 00 if there are three bytes and it is 00 - if len(a) == 3 and a[0] == '00': - a.remove(0); - - # if we have two or three bytes now, and the first character is a 0, remove it - if len(a) >= 2 and a[0][0:1] == '0': - a[0] = a[0][1]; - - result = '' - for item in a: - result += ('.' if result != '' else '') + item - - - result = 'v' + result - - return result - - -def get_free_space(path): - result = -1 - - try: - s = statvfs(path) - result = s.f_frsize * s.f_bavail # Number of free bytes that ordinary users - except Exception as ex: - logger.info("Error while retrieving free space for path %s: %s" % (path, ex)) - - return result - - -def _get_sysfs_machine_name(): - try: - with open('/sys/firmware/devicetree/base/model', 'r') as f: - return f.read().rstrip('\x00') - except IOError: - pass - - return None - -# Returns None if it cannot find a machine name. Otherwise returns the string -# containing the name -def get_machine_name(): - # First try calling the venus utility script - try: - return check_output("/usr/bin/product-name").strip().decode('UTF-8') - except (CalledProcessError, OSError): - pass - - # Fall back to sysfs - name = _get_sysfs_machine_name() - if name is not None: - return name - - # Fall back to venus build machine name - try: - with open('/etc/venus/machine', 'r', encoding='UTF-8') as f: - return f.read().strip() - except IOError: - pass - - return None - - -def get_product_id(): - """ Find the machine ID and return it. """ - - # First try calling the venus utility script - try: - return check_output("/usr/bin/product-id").strip().decode('UTF-8') - except (CalledProcessError, OSError): - pass - - # Fall back machine name mechanism - name = _get_sysfs_machine_name() - return { - 'Color Control GX': 'C001', - 'Venus GX': 'C002', - 'Octo GX': 'C006', - 'EasySolar-II': 'C007', - 'MultiPlus-II': 'C008', - 'Maxi GX': 'C009', - 'Cerbo GX': 'C00A' - }.get(name, 'C003') # C003 is Generic - - -# Returns False if it cannot open the file. Otherwise returns its rstripped contents -def read_file(path): - content = False - - try: - with open(path, 'r') as f: - content = f.read().rstrip() - except Exception as ex: - logger.debug("Error while reading %s: %s" % (path, ex)) - - return content - - -def wrap_dbus_value(value): - if value is None: - return VEDBUS_INVALID - if isinstance(value, float): - return dbus.Double(value, variant_level=1) - if isinstance(value, bool): - return dbus.Boolean(value, variant_level=1) - if isinstance(value, int): - try: - return dbus.Int32(value, variant_level=1) - except OverflowError: - return dbus.Int64(value, variant_level=1) - if isinstance(value, str): - return dbus.String(value, variant_level=1) - if isinstance(value, list): - if len(value) == 0: - # If the list is empty we cannot infer the type of the contents. So assume unsigned integer. - # A (signed) integer is dangerous, because an empty list of signed integers is used to encode - # an invalid value. - return dbus.Array([], signature=dbus.Signature('u'), variant_level=1) - return dbus.Array([wrap_dbus_value(x) for x in value], variant_level=1) - if isinstance(value, dict): - # Wrapping the keys of the dictionary causes D-Bus errors like: - # 'arguments to dbus_message_iter_open_container() were incorrect, - # assertion "(type == DBUS_TYPE_ARRAY && contained_signature && - # *contained_signature == DBUS_DICT_ENTRY_BEGIN_CHAR) || (contained_signature == NULL || - # _dbus_check_is_valid_signature (contained_signature))" failed in file ...' - return dbus.Dictionary({(k, wrap_dbus_value(v)) for k, v in value.items()}, variant_level=1) - return value - - -dbus_int_types = (dbus.Int32, dbus.UInt32, dbus.Byte, dbus.Int16, dbus.UInt16, dbus.UInt32, dbus.Int64, dbus.UInt64) - - -def unwrap_dbus_value(val): - """Converts D-Bus values back to the original type. For example if val is of type DBus.Double, - a float will be returned.""" - if isinstance(val, dbus_int_types): - return int(val) - if isinstance(val, dbus.Double): - return float(val) - if isinstance(val, dbus.Array): - v = [unwrap_dbus_value(x) for x in val] - return None if len(v) == 0 else v - if isinstance(val, (dbus.Signature, dbus.String)): - return str(val) - # Python has no byte type, so we convert to an integer. - if isinstance(val, dbus.Byte): - return int(val) - if isinstance(val, dbus.ByteArray): - return "".join([bytes(x) for x in val]) - if isinstance(val, (list, tuple)): - return [unwrap_dbus_value(x) for x in val] - if isinstance(val, (dbus.Dictionary, dict)): - # Do not unwrap the keys, see comment in wrap_dbus_value - return dict([(x, unwrap_dbus_value(y)) for x, y in val.items()]) - if isinstance(val, dbus.Boolean): - return bool(val) - return val - -# When supported, only name owner changes for the the given namespace are reported. This -# prevents spending cpu time at irrelevant changes, like scripts accessing the bus temporarily. -def add_name_owner_changed_receiver(dbus, name_owner_changed, namespace="com.victronenergy"): - # support for arg0namespace is submitted upstream, but not included at the time of - # writing, Venus OS does support it, so try if it works. - if namespace is None: - dbus.add_signal_receiver(name_owner_changed, signal_name='NameOwnerChanged') - else: - try: - dbus.add_signal_receiver(name_owner_changed, - signal_name='NameOwnerChanged', arg0namespace=namespace) - except TypeError: - dbus.add_signal_receiver(name_owner_changed, signal_name='NameOwnerChanged') diff --git a/velib_python/v3.40~38/vedbus.py b/velib_python/v3.40~38/vedbus.py deleted file mode 100644 index 0407f6c..0000000 --- a/velib_python/v3.40~38/vedbus.py +++ /dev/null @@ -1,642 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import dbus.service -import logging -import traceback -import os -import weakref -from collections import defaultdict -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 = {} - self._ratelimiters = [] - self._dbusname = None - self.name = servicename - - # 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 - - # Add the root item that will return all items as a tree - self._dbusnodes['/'] = VeDbusRootExport(self._dbusconn, '/', self) - - def register(self): - # Register ourselves on the dbus, trigger an error if already in use (do_not_queue) - self._dbusname = dbus.service.BusName(self.name, self._dbusconn, do_not_queue=True) - logging.info("registered ourselves on D-Bus as %s" % self.name) - - # To force immediate deregistering of this dbus service and all its object paths, explicitly - # call __del__(). - def __del__(self): - for node in list(self._dbusnodes.values()): - node.__del__() - self._dbusnodes.clear() - for item in list(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 - - def get_name(self): - return self._dbusname.get_name() - - # @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, valuetype=None, itemtype=None): - - if onchangecallback is not None: - self._onchangecallbacks[path] = onchangecallback - - itemtype = itemtype or VeDbusItemExport - item = itemtype(self._dbusconn, path, value, description, writeable, - self._value_changed, gettextcallback, deletecallback=self._item_deleted, valuetype=valuetype) - - 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] = VeDbusTreeExport(self._dbusconn, subPath, self) - self._dbusobjects[path] = item - logging.debug('added %s with start value %s. Writeable is %s' % (path, value, writeable)) - return item - - # 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) - - # 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 list(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 - - def __enter__(self): - l = ServiceContext(self) - self._ratelimiters.append(l) - return l - - def __exit__(self, *exc): - # pop off the top one and flush it. If with statements are nested - # then each exit flushes its own part. - if self._ratelimiters: - self._ratelimiters.pop().flush() - -class ServiceContext(object): - def __init__(self, parent): - self.parent = parent - self.changes = {} - - def __contains__(self, path): - return path in self.parent - - def __getitem__(self, path): - return self.parent[path] - - def __setitem__(self, path, newvalue): - c = self.parent._dbusobjects[path]._local_set_value(newvalue) - if c is not None: - self.changes[path] = c - - def __delitem__(self, path): - if path in self.changes: - del self.changes[path] - del self.parent[path] - - def flush(self): - if self.changes: - self.parent._dbusnodes['/'].ItemsChanged(self.changes) - self.changes.clear() - - def add_path(self, path, value, *args, **kwargs): - self.parent.add_path(path, value, *args, **kwargs) - self.changes[path] = { - 'Value': wrap_dbus_value(value), - 'Text': self.parent._dbusobjects[path].GetText() - } - - def del_tree(self, root): - root = root.rstrip('/') - for p in list(self.parent._dbusobjects.keys()): - if p == root or p.startswith(root + '/'): - self[p] = None - self.parent._dbusobjects[p].__del__() - - def get_name(self): - return self.parent.get_name() - -class TrackerDict(defaultdict): - """ Same as defaultdict, but passes the key to default_factory. """ - def __missing__(self, key): - self[key] = x = self.default_factory(key) - return x - -class VeDbusRootTracker(object): - """ This tracks the root of a dbus path and listens for PropertiesChanged - signals. When a signal arrives, parse it and unpack the key/value changes - into traditional events, then pass it to the original eventCallback - method. """ - def __init__(self, bus, serviceName): - self.importers = defaultdict(weakref.WeakSet) - self.serviceName = serviceName - self._match = bus.get_object(serviceName, '/', introspect=False).connect_to_signal( - "ItemsChanged", weak_functor(self._items_changed_handler)) - - def __del__(self): - self._match.remove() - self._match = None - - def add(self, i): - self.importers[i.path].add(i) - - def _items_changed_handler(self, items): - if not isinstance(items, dict): - return - - for path, changes in items.items(): - try: - v = changes['Value'] - except KeyError: - continue - - try: - t = changes['Text'] - except KeyError: - t = str(unwrap_dbus_value(v)) - - for i in self.importers.get(path, ()): - i._properties_changed_handler({'Value': v, 'Text': t}) - -""" -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): - def __new__(cls, bus, serviceName, path, eventCallback=None, createsignal=True): - instance = object.__new__(cls) - - # If signal tracking should be done, also add to root tracker - if createsignal: - if "_roots" not in cls.__dict__: - cls._roots = TrackerDict(lambda k: VeDbusRootTracker(bus, k)) - - return instance - - ## 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)) - self._roots[serviceName].add(self) - - # 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 is not 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 - - ## Resets the item to its default value - def set_default(self): - self._proxy.SetDefault() - self._refreshcachedvalue() - - ## 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, service): - dbus.service.Object.__init__(self, bus, objectPath) - self._service = service - 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] - - def _get_value_handler(self, path, get_text=False): - logging.debug("_get_value_handler called for %s" % path) - r = {} - px = path - if not px.endswith('/'): - px += '/' - for p, item in self._service._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 - - @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 VeDbusRootExport(VeDbusTreeExport): - @dbus.service.signal('com.victronenergy.BusItem', signature='a{sa{sv}}') - def ItemsChanged(self, changes): - pass - - @dbus.service.method('com.victronenergy.BusItem', out_signature='a{sa{sv}}') - def GetItems(self): - return { - path: { - 'Value': wrap_dbus_value(item.local_get_value()), - 'Text': item.GetText() } - for path, item in self._service._dbusobjects.items() - } - - -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, - valuetype=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 - self._type = valuetype - - # 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.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): - changes = self._local_set_value(newvalue) - if changes is not None: - self.PropertiesChanged(changes) - - def _local_set_value(self, newvalue): - if self._value == newvalue: - return None - - self._value = newvalue - return { - 'Value': wrap_dbus_value(newvalue), - 'Text': self.GetText() - } - - 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 value type is enforced, cast it. If the type can be coerced - # python will do it for us. This allows ints to become floats, - # or bools to become ints. Additionally also allow None, so that - # a path may be invalidated. - if self._type is not None and newvalue is not None: - try: - newvalue = self._type(newvalue) - except (ValueError, TypeError): - return 1 # NOT OK - - 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) diff --git a/velib_python/latest/ve_utils.py b/velib_python/ve_utils.py similarity index 100% rename from velib_python/latest/ve_utils.py rename to velib_python/ve_utils.py diff --git a/velib_python/latest/vedbus.py b/velib_python/vedbus.py similarity index 100% rename from velib_python/latest/vedbus.py rename to velib_python/vedbus.py diff --git a/velib_python/velib_python/latest/dbusmonitor.py b/velib_python/velib_python/latest/dbusmonitor.py deleted file mode 100644 index fd25700..0000000 --- a/velib_python/velib_python/latest/dbusmonitor.py +++ /dev/null @@ -1,587 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -## @package dbus_vrm -# This code takes care of the D-Bus interface (not all of below is implemented yet): -# - on startup it scans the dbus for services we know. For each known service found, it searches for -# objects/paths we know. Everything we find is stored in items{}, and an event is registered: if a -# value changes weĺl be notified and can pass that on to our owner. For example the vrmLogger. -# we know. -# - after startup, it continues to monitor the dbus: -# 1) when services are added we do the same check on that -# 2) when services are removed, we remove any items that we had that referred to that service -# 3) if an existing services adds paths we update ourselves as well: on init, we make a -# VeDbusItemImport for a non-, or not yet existing objectpaths as well1 -# -# Code is used by the vrmLogger, and also the pubsub code. Both are other modules in the dbus_vrm repo. - -from dbus.mainloop.glib import DBusGMainLoop -from gi.repository import GLib -import dbus -import dbus.service -import inspect -import logging -import argparse -import pprint -import traceback -import os -from collections import defaultdict -from functools import partial - -# our own packages -from ve_utils import exit_on_error, wrap_dbus_value, unwrap_dbus_value, add_name_owner_changed_receiver -notfound = object() # For lookups where None is a valid result - -logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) -class SystemBus(dbus.bus.BusConnection): - def __new__(cls): - return dbus.bus.BusConnection.__new__(cls, dbus.bus.BusConnection.TYPE_SYSTEM) - -class SessionBus(dbus.bus.BusConnection): - def __new__(cls): - return dbus.bus.BusConnection.__new__(cls, dbus.bus.BusConnection.TYPE_SESSION) - -class MonitoredValue(object): - def __init__(self, value, text, options): - super(MonitoredValue, self).__init__() - self.value = value - self.text = text - self.options = options - - # For legacy code, allow treating this as a tuple/list - def __iter__(self): - return iter((self.value, self.text, self.options)) - -class Service(object): - def __init__(self, id, serviceName, deviceInstance): - super(Service, self).__init__() - self.id = id - self.name = serviceName - self.paths = {} - self._seen = set() - self.deviceInstance = deviceInstance - - # For legacy code, attributes can still be accessed as if keys from a - # dictionary. - def __setitem__(self, key, value): - self.__dict__[key] = value - def __getitem__(self, key): - return self.__dict__[key] - - def set_seen(self, path): - self._seen.add(path) - - def seen(self, path): - return path in self._seen - - @property - def service_class(self): - return '.'.join(self.name.split('.')[:3]) - -class DbusMonitor(object): - ## Constructor - def __init__(self, dbusTree, valueChangedCallback=None, deviceAddedCallback=None, - deviceRemovedCallback=None, namespace="com.victronenergy", ignoreServices=[]): - # valueChangedCallback is the callback that we call when something has changed. - # def value_changed_on_dbus(dbusServiceName, dbusPath, options, changes, deviceInstance): - # in which changes is a tuple with GetText() and GetValue() - self.valueChangedCallback = valueChangedCallback - self.deviceAddedCallback = deviceAddedCallback - self.deviceRemovedCallback = deviceRemovedCallback - self.dbusTree = dbusTree - self.ignoreServices = ignoreServices - - # Lists all tracked services. Stores name, id, device instance, value per path, and whenToLog info - # indexed by service name (eg. com.victronenergy.settings). - self.servicesByName = {} - - # Same values as self.servicesByName, but indexed by service id (eg. :1.30) - self.servicesById = {} - - # Keep track of services by class to speed up calls to get_service_list - self.servicesByClass = defaultdict(list) - - # Keep track of any additional watches placed on items - self.serviceWatches = defaultdict(list) - - # For a PC, connect to the SessionBus - # For a CCGX, connect to the SystemBus - self.dbusConn = SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else SystemBus() - - # subscribe to NameOwnerChange for bus connect / disconnect events. - # NOTE: this is on a different bus then the one above! - standardBus = (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ \ - else dbus.SystemBus()) - - add_name_owner_changed_receiver(standardBus, self.dbus_name_owner_changed) - - # Subscribe to PropertiesChanged for all services - self.dbusConn.add_signal_receiver(self.handler_value_changes, - dbus_interface='com.victronenergy.BusItem', - signal_name='PropertiesChanged', path_keyword='path', - sender_keyword='senderId') - - # Subscribe to ItemsChanged for all services - self.dbusConn.add_signal_receiver(self.handler_item_changes, - dbus_interface='com.victronenergy.BusItem', - signal_name='ItemsChanged', path='/', - sender_keyword='senderId') - - logger.info('===== Search on dbus for services that we will monitor starting... =====') - serviceNames = self.dbusConn.list_names() - for serviceName in serviceNames: - self.scan_dbus_service(serviceName) - - logger.info('===== Search on dbus for services that we will monitor finished =====') - - @staticmethod - def make_service(serviceId, serviceName, deviceInstance): - """ Override this to use a different kind of service object. """ - return Service(serviceId, serviceName, deviceInstance) - - def make_monitor(self, service, path, value, text, options): - """ Override this to do more things with monitoring. """ - return MonitoredValue(unwrap_dbus_value(value), unwrap_dbus_value(text), options) - - def dbus_name_owner_changed(self, name, oldowner, newowner): - if not name.startswith("com.victronenergy."): - return - - #decouple, and process in main loop - GLib.idle_add(exit_on_error, self._process_name_owner_changed, name, oldowner, newowner) - - def _process_name_owner_changed(self, name, oldowner, newowner): - if newowner != '': - # so we found some new service. Check if we can do something with it. - newdeviceadded = self.scan_dbus_service(name) - if newdeviceadded and self.deviceAddedCallback is not None: - self.deviceAddedCallback(name, self.get_device_instance(name)) - - elif name in self.servicesByName: - # it disappeared, we need to remove it. - logger.info("%s disappeared from the dbus. Removing it from our lists" % name) - service = self.servicesByName[name] - del self.servicesById[service.id] - del self.servicesByName[name] - for watch in self.serviceWatches[name]: - watch.remove() - del self.serviceWatches[name] - self.servicesByClass[service.service_class].remove(service) - if self.deviceRemovedCallback is not None: - self.deviceRemovedCallback(name, service.deviceInstance) - - def scan_dbus_service(self, serviceName): - try: - return self.scan_dbus_service_inner(serviceName) - except: - logger.error("Ignoring %s because of error while scanning:" % (serviceName)) - traceback.print_exc() - return False - - # Errors 'org.freedesktop.DBus.Error.ServiceUnknown' and - # 'org.freedesktop.DBus.Error.Disconnected' seem to happen when the service - # disappears while its being scanned. Which might happen, but is not really - # normal either, so letting them go into the logs. - - # Scans the given dbus service to see if it contains anything interesting for us. If it does, add - # it to our list of monitored D-Bus services. - def scan_dbus_service_inner(self, serviceName): - - # make it a normal string instead of dbus string - serviceName = str(serviceName) - - if (len(self.ignoreServices) != 0 and any(serviceName.startswith(x) for x in self.ignoreServices)): - logger.debug("Ignoring service %s" % serviceName) - return False - - paths = self.dbusTree.get('.'.join(serviceName.split('.')[0:3]), None) - if paths is None: - logger.debug("Ignoring service %s, not in the tree" % serviceName) - return False - - logger.info("Found: %s, scanning and storing items" % serviceName) - serviceId = self.dbusConn.get_name_owner(serviceName) - - # we should never be notified to add a D-Bus service that we already have. If this assertion - # raises, check process_name_owner_changed, and D-Bus workings. - assert serviceName not in self.servicesByName - assert serviceId not in self.servicesById - - # Try to fetch everything with a GetItems, then fall back to older - # methods if that fails - try: - values = self.dbusConn.call_blocking(serviceName, '/', None, 'GetItems', '', []) - except dbus.exceptions.DBusException: - logger.info("GetItems failed, trying legacy methods") - else: - return self.scan_dbus_service_getitems_done(serviceName, serviceId, values) - - if serviceName == 'com.victronenergy.settings': - di = 0 - elif serviceName.startswith('com.victronenergy.vecan.'): - di = 0 - else: - try: - di = self.dbusConn.call_blocking(serviceName, - '/DeviceInstance', None, 'GetValue', '', []) - except dbus.exceptions.DBusException: - logger.info(" %s was skipped because it has no device instance" % serviceName) - return False # Skip it - else: - di = int(di) - - logger.info(" %s has device instance %s" % (serviceName, di)) - service = self.make_service(serviceId, serviceName, di) - - # Let's try to fetch everything in one go - values = {} - texts = {} - try: - values.update(self.dbusConn.call_blocking(serviceName, '/', None, 'GetValue', '', [])) - texts.update(self.dbusConn.call_blocking(serviceName, '/', None, 'GetText', '', [])) - except: - pass - - for path, options in paths.items(): - # path will be the D-Bus path: '/Ac/ActiveIn/L1/V' - # options will be a dictionary: {'code': 'V', 'whenToLog': 'onIntervalAlways'} - - # Try to obtain the value we want from our bulk fetch. If we - # cannot find it there, do an individual query. - value = values.get(path[1:], notfound) - if value != notfound: - service.set_seen(path) - text = texts.get(path[1:], notfound) - if value is notfound or text is notfound: - try: - value = self.dbusConn.call_blocking(serviceName, path, None, 'GetValue', '', []) - service.set_seen(path) - text = self.dbusConn.call_blocking(serviceName, path, None, 'GetText', '', []) - except dbus.exceptions.DBusException as e: - if e.get_dbus_name() in ( - 'org.freedesktop.DBus.Error.ServiceUnknown', - 'org.freedesktop.DBus.Error.Disconnected'): - raise # This exception will be handled below - - # TODO org.freedesktop.DBus.Error.UnknownMethod really - # shouldn't happen but sometimes does. - logger.debug("%s %s does not exist (yet)" % (serviceName, path)) - value = None - text = None - - service.paths[path] = self.make_monitor(service, path, unwrap_dbus_value(value), unwrap_dbus_value(text), options) - - - logger.debug("Finished scanning and storing items for %s" % serviceName) - - # Adjust self at the end of the scan, so we don't have an incomplete set of - # data if an exception occurs during the scan. - self.servicesByName[serviceName] = service - self.servicesById[serviceId] = service - self.servicesByClass[service.service_class].append(service) - - return True - - def scan_dbus_service_getitems_done(self, serviceName, serviceId, values): - # Keeping these exceptions for legacy reasons - if serviceName == 'com.victronenergy.settings': - di = 0 - elif serviceName.startswith('com.victronenergy.vecan.'): - di = 0 - else: - try: - di = values['/DeviceInstance']['Value'] - except KeyError: - logger.info(" %s was skipped because it has no device instance" % serviceName) - return False - else: - di = int(di) - - logger.info(" %s has device instance %s" % (serviceName, di)) - service = self.make_service(serviceId, serviceName, di) - - paths = self.dbusTree.get('.'.join(serviceName.split('.')[0:3]), {}) - for path, options in paths.items(): - item = values.get(path, notfound) - if item is notfound: - service.paths[path] = self.make_monitor(service, path, None, None, options) - else: - service.set_seen(path) - value = item.get('Value', None) - text = item.get('Text', None) - service.paths[path] = self.make_monitor(service, path, unwrap_dbus_value(value), unwrap_dbus_value(text), options) - - self.servicesByName[serviceName] = service - self.servicesById[serviceId] = service - self.servicesByClass[service.service_class].append(service) - return True - - def handler_item_changes(self, items, senderId): - if not isinstance(items, dict): - return - - try: - service = self.servicesById[senderId] - except KeyError: - # senderId isn't there, which means it hasn't been scanned yet. - return - - for path, changes in items.items(): - try: - v = unwrap_dbus_value(changes['Value']) - except (KeyError, TypeError): - continue - - try: - t = changes['Text'] - except KeyError: - t = str(v) - self._handler_value_changes(service, path, v, t) - - def handler_value_changes(self, changes, path, senderId): - # If this properyChange does not involve a value, our work is done. - if 'Value' not in changes: - return - - try: - service = self.servicesById[senderId] - except KeyError: - # senderId isn't there, which means it hasn't been scanned yet. - return - - v = unwrap_dbus_value(changes['Value']) - # Some services don't send Text with their PropertiesChanged events. - try: - t = changes['Text'] - except KeyError: - t = str(v) - self._handler_value_changes(service, path, v, t) - - def _handler_value_changes(self, service, path, value, text): - try: - a = service.paths[path] - except KeyError: - # path isn't there, which means it hasn't been scanned yet. - return - - service.set_seen(path) - - # First update our store to the new value - if a.value == value: - return - - a.value = value - a.text = text - - # And do the rest of the processing in on the mainloop - if self.valueChangedCallback is not None: - GLib.idle_add(exit_on_error, self._execute_value_changes, service.name, path, { - 'Value': value, 'Text': text}, a.options) - - def _execute_value_changes(self, serviceName, objectPath, changes, options): - # double check that the service still exists, as it might have - # disappeared between scheduling-for and executing this function. - if serviceName not in self.servicesByName: - return - - self.valueChangedCallback(serviceName, objectPath, - options, changes, self.get_device_instance(serviceName)) - - # Gets the value for a certain servicename and path - # The default_value is returned when: - # 1. When the service doesn't exist. - # 2. When the path asked for isn't being monitored. - # 3. When the path exists, but has dbus-invalid, ie an empty byte array. - # 4. When the path asked for is being monitored, but doesn't exist for that service. - def get_value(self, serviceName, objectPath, default_value=None): - service = self.servicesByName.get(serviceName, None) - if service is None: - return default_value - - value = service.paths.get(objectPath, None) - if value is None or value.value is None: - return default_value - - return value.value - - # returns if a dbus exists now, by doing a blocking dbus call. - # Typically seen will be sufficient and doesn't need access to the dbus. - def exists(self, serviceName, objectPath): - try: - self.dbusConn.call_blocking(serviceName, objectPath, None, 'GetValue', '', []) - return True - except dbus.exceptions.DBusException as e: - return False - - # Returns if there ever was a successful GetValue or valueChanged event. - # Unlike get_value this return True also if the actual value is invalid. - # - # Note: the path might no longer exists anymore, but that doesn't happen in - # practice. If a service really wants to reconfigure itself typically it should - # reconnect to the dbus which causes it to be rescanned and seen will be updated. - # If it is really needed to know if a path still exists, use exists. - def seen(self, serviceName, objectPath): - try: - return self.servicesByName[serviceName].seen(objectPath) - except KeyError: - return False - - # Sets the value for a certain servicename and path, returns the return value of the D-Bus SetValue - # method. If the underlying item does not exist (the service does not exist, or the objectPath was not - # registered) the function will return -1 - def set_value(self, serviceName, objectPath, value): - # Check if the D-Bus object referenced by serviceName and objectPath is registered. There is no - # necessity to do this, but it is in line with previous implementations which kept VeDbusItemImport - # objects for registers items only. - service = self.servicesByName.get(serviceName, None) - if service is None: - return -1 - if objectPath not in service.paths: - return -1 - # We do not catch D-Bus exceptions here, because the previous implementation did not do that either. - return self.dbusConn.call_blocking(serviceName, objectPath, - dbus_interface='com.victronenergy.BusItem', - method='SetValue', signature=None, - args=[wrap_dbus_value(value)]) - - # Similar to set_value, but operates asynchronously - def set_value_async(self, serviceName, objectPath, value, - reply_handler=None, error_handler=None): - service = self.servicesByName.get(serviceName, None) - if service is not None: - if objectPath in service.paths: - self.dbusConn.call_async(serviceName, objectPath, - dbus_interface='com.victronenergy.BusItem', - method='SetValue', signature=None, - args=[wrap_dbus_value(value)], - reply_handler=reply_handler, error_handler=error_handler) - return - - if error_handler is not None: - error_handler(TypeError('Service or path not found, ' - 'service=%s, path=%s' % (serviceName, objectPath))) - - # returns a dictionary, keys are the servicenames, value the instances - # optionally use the classfilter to get only a certain type of services, for - # example com.victronenergy.battery. - def get_service_list(self, classfilter=None): - if classfilter is None: - return { servicename: service.deviceInstance \ - for servicename, service in self.servicesByName.items() } - - if classfilter not in self.servicesByClass: - return {} - - return { service.name: service.deviceInstance \ - for service in self.servicesByClass[classfilter] } - - def get_device_instance(self, serviceName): - return self.servicesByName[serviceName].deviceInstance - - def track_value(self, serviceName, objectPath, callback, *args, **kwargs): - """ A DbusMonitor can watch specific service/path combos for changes - so that it is not fully reliant on the global handler_value_changes - in this class. Additional watches are deleted automatically when - the service disappears from dbus. """ - cb = partial(callback, *args, **kwargs) - - def root_tracker(items): - # Check if objectPath in dict - try: - v = items[objectPath] - _v = unwrap_dbus_value(v['Value']) - except (KeyError, TypeError): - return # not in this dict - - try: - t = v['Text'] - except KeyError: - cb({'Value': _v }) - else: - cb({'Value': _v, 'Text': t}) - - # Track changes on the path, and also on root - self.serviceWatches[serviceName].extend(( - self.dbusConn.add_signal_receiver(cb, - dbus_interface='com.victronenergy.BusItem', - signal_name='PropertiesChanged', - path=objectPath, bus_name=serviceName), - self.dbusConn.add_signal_receiver(root_tracker, - dbus_interface='com.victronenergy.BusItem', - signal_name='ItemsChanged', - path="/", bus_name=serviceName), - )) - - -# ====== ALL CODE BELOW THIS LINE IS PURELY FOR DEVELOPING THIS CLASS ====== - -# Example function that can be used as a starting point to use this code -def value_changed_on_dbus(dbusServiceName, dbusPath, dict, changes, deviceInstance): - logger.debug("0 ----------------") - logger.debug("1 %s%s changed" % (dbusServiceName, dbusPath)) - logger.debug("2 vrm dict : %s" % dict) - logger.debug("3 changes-text: %s" % changes['Text']) - logger.debug("4 changes-value: %s" % changes['Value']) - logger.debug("5 deviceInstance: %s" % deviceInstance) - logger.debug("6 - end") - - -def nameownerchange(a, b): - # used to find memory leaks in dbusmonitor and VeDbusItemImport - import gc - gc.collect() - objects = gc.get_objects() - print (len([o for o in objects if type(o).__name__ == 'VeDbusItemImport'])) - print (len([o for o in objects if type(o).__name__ == 'SignalMatch'])) - print (len(objects)) - - -def print_values(dbusmonitor): - a = dbusmonitor.get_value('wrongservice', '/DbusInvalid', default_value=1000) - b = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/NotInTheMonitorList', default_value=1000) - c = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/DbusInvalid', default_value=1000) - d = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/NonExistingButMonitored', default_value=1000) - - print ("All should be 1000: Wrong Service: %s, NotInTheMonitorList: %s, DbusInvalid: %s, NonExistingButMonitored: %s" % (a, b, c, d)) - return True - -# We have a mainloop, but that is just for developing this code. Normally above class & code is used from -# some other class, such as vrmLogger or the pubsub Implementation. -def main(): - # Init logging - logging.basicConfig(level=logging.DEBUG) - logger.info(__file__ + " is starting up") - - # Have a mainloop, so we can send/receive asynchronous calls to and from dbus - DBusGMainLoop(set_as_default=True) - - import os - import sys - sys.path.insert(1, os.path.join(os.path.dirname(__file__), '../../')) - - dummy = {'code': None, 'whenToLog': 'configChange', 'accessLevel': None} - monitorlist = {'com.victronenergy.dummyservice': { - '/Connected': dummy, - '/ProductName': dummy, - '/Mgmt/Connection': dummy, - '/Dc/0/Voltage': dummy, - '/Dc/0/Current': dummy, - '/Dc/0/Temperature': dummy, - '/Load/I': dummy, - '/FirmwareVersion': dummy, - '/DbusInvalid': dummy, - '/NonExistingButMonitored': dummy}} - - d = DbusMonitor(monitorlist, value_changed_on_dbus, - deviceAddedCallback=nameownerchange, deviceRemovedCallback=nameownerchange) - - GLib.timeout_add(1000, print_values, d) - - # Start and run the mainloop - logger.info("Starting mainloop, responding on only events") - mainloop = GLib.MainLoop() - mainloop.run() - -if __name__ == "__main__": - main() diff --git a/velib_python/velib_python/latest/oldestVersion b/velib_python/velib_python/latest/oldestVersion deleted file mode 100644 index bdea676..0000000 --- a/velib_python/velib_python/latest/oldestVersion +++ /dev/null @@ -1 +0,0 @@ -v3.40~39 diff --git a/velib_python/velib_python/latest/settingsdevice.py b/velib_python/velib_python/latest/settingsdevice.py deleted file mode 100644 index a207e8b..0000000 --- a/velib_python/velib_python/latest/settingsdevice.py +++ /dev/null @@ -1,118 +0,0 @@ -import dbus -import logging -import time -from functools import partial - -# Local imports -from vedbus import VeDbusItemImport - -## Indexes for the setting dictonary. -PATH = 0 -VALUE = 1 -MINIMUM = 2 -MAXIMUM = 3 -SILENT = 4 - -## The Settings Device class. -# Used by python programs, such as the vrm-logger, to read and write settings they -# need to store on disk. And since these settings might be changed from a different -# source, such as the GUI, the program can pass an eventCallback that will be called -# as soon as some setting is changed. -# -# The settings are stored in flash via the com.victronenergy.settings service on dbus. -# See https://github.com/victronenergy/localsettings for more info. -# -# If there are settings in de supportSettings list which are not yet on the dbus, -# and therefore not yet in the xml file, they will be added through the dbus-addSetting -# interface of com.victronenergy.settings. -class SettingsDevice(object): - ## The constructor processes the tree of dbus-items. - # @param bus the system-dbus object - # @param name the dbus-service-name of the settings dbus service, 'com.victronenergy.settings' - # @param supportedSettings dictionary with all setting-names, and their defaultvalue, min, max and whether - # the setting is silent. The 'silent' entry is optional. If set to true, no changes in the setting will - # be logged by localsettings. - # @param eventCallback function that will be called on changes on any of these settings - # @param timeout Maximum interval to wait for localsettings. An exception is thrown at the end of the - # interval if the localsettings D-Bus service has not appeared yet. - def __init__(self, bus, supportedSettings, eventCallback, name='com.victronenergy.settings', timeout=0): - logging.debug("===== Settings device init starting... =====") - self._bus = bus - self._dbus_name = name - self._eventCallback = eventCallback - self._values = {} # stored the values, used to pass the old value along on a setting change - self._settings = {} - - count = 0 - while True: - if 'com.victronenergy.settings' in self._bus.list_names(): - break - if count == timeout: - raise Exception("The settings service com.victronenergy.settings does not exist!") - count += 1 - logging.info('waiting for settings') - time.sleep(1) - - # Add the items. - self.addSettings(supportedSettings) - - logging.debug("===== Settings device init finished =====") - - def addSettings(self, settings): - for setting, options in settings.items(): - silent = len(options) > SILENT and options[SILENT] - busitem = self.addSetting(options[PATH], options[VALUE], - options[MINIMUM], options[MAXIMUM], silent, callback=partial(self.handleChangedSetting, setting)) - self._settings[setting] = busitem - self._values[setting] = busitem.get_value() - - def addSetting(self, path, value, _min, _max, silent=False, callback=None): - busitem = VeDbusItemImport(self._bus, self._dbus_name, path, callback) - if busitem.exists and (value, _min, _max, silent) == busitem._proxy.GetAttributes(): - logging.debug("Setting %s found" % path) - else: - logging.info("Setting %s does not exist yet or must be adjusted" % path) - - # Prepare to add the setting. Most dbus types extend the python - # type so it is only necessary to additionally test for Int64. - if isinstance(value, (int, dbus.Int64)): - itemType = 'i' - elif isinstance(value, float): - itemType = 'f' - else: - itemType = 's' - - # Add the setting - # TODO, make an object that inherits VeDbusItemImport, and complete the D-Bus settingsitem interface - settings_item = VeDbusItemImport(self._bus, self._dbus_name, '/Settings', createsignal=False) - setting_path = path.replace('/Settings/', '', 1) - if silent: - settings_item._proxy.AddSilentSetting('', setting_path, value, itemType, _min, _max) - else: - settings_item._proxy.AddSetting('', setting_path, value, itemType, _min, _max) - - busitem = VeDbusItemImport(self._bus, self._dbus_name, path, callback) - - return busitem - - def handleChangedSetting(self, setting, servicename, path, changes): - oldvalue = self._values[setting] if setting in self._values else None - self._values[setting] = changes['Value'] - - if self._eventCallback is None: - return - - self._eventCallback(setting, oldvalue, changes['Value']) - - def setDefault(self, path): - item = VeDbusItemImport(self._bus, self._dbus_name, path, createsignal=False) - item.set_default() - - def __getitem__(self, setting): - return self._settings[setting].get_value() - - def __setitem__(self, setting, newvalue): - result = self._settings[setting].set_value(newvalue) - if result != 0: - # Trying to make some false change to our own settings? How dumb! - assert False diff --git a/velib_python/velib_python/latest/ve_utils.py b/velib_python/velib_python/latest/ve_utils.py deleted file mode 100644 index f5a2f85..0000000 --- a/velib_python/velib_python/latest/ve_utils.py +++ /dev/null @@ -1,276 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -import sys -from traceback import print_exc -from os import _exit as os_exit -from os import statvfs -from subprocess import check_output, CalledProcessError -import logging -import dbus -logger = logging.getLogger(__name__) - -VEDBUS_INVALID = dbus.Array([], signature=dbus.Signature('i'), variant_level=1) - -class NoVrmPortalIdError(Exception): - pass - -# Use this function to make sure the code quits on an unexpected exception. Make sure to use it -# when using GLib.idle_add and also GLib.timeout_add. -# Without this, the code will just keep running, since GLib does not stop the mainloop on an -# exception. -# Example: GLib.idle_add(exit_on_error, myfunc, arg1, arg2) -def exit_on_error(func, *args, **kwargs): - try: - return func(*args, **kwargs) - except: - try: - print ('exit_on_error: there was an exception. Printing stacktrace will be tried and then exit') - print_exc() - except: - pass - - # sys.exit() is not used, since that throws an exception, which does not lead to a program - # halt when used in a dbus callback, see connection.py in the Python/Dbus libraries, line 230. - os_exit(1) - - -__vrm_portal_id = None -def get_vrm_portal_id(): - # The original definition of the VRM Portal ID is that it is the mac - # address of the onboard- ethernet port (eth0), stripped from its colons - # (:) and lower case. This may however differ between platforms. On Venus - # the task is therefore deferred to /sbin/get-unique-id so that a - # platform specific method can be easily defined. - # - # If /sbin/get-unique-id does not exist, then use the ethernet address - # of eth0. This also handles the case where velib_python is used as a - # package install on a Raspberry Pi. - # - # On a Linux host where the network interface may not be eth0, you can set - # the VRM_IFACE environment variable to the correct name. - - global __vrm_portal_id - - if __vrm_portal_id: - return __vrm_portal_id - - portal_id = None - - # First try the method that works if we don't have a data partition. This - # will fail when the current user is not root. - try: - portal_id = check_output("/sbin/get-unique-id").decode("utf-8", "ignore").strip() - if not portal_id: - raise NoVrmPortalIdError("get-unique-id returned blank") - __vrm_portal_id = portal_id - return portal_id - except CalledProcessError: - # get-unique-id returned non-zero - raise NoVrmPortalIdError("get-unique-id returned non-zero") - except OSError: - # File doesn't exist, use fallback - pass - - # Fall back to getting our id using a syscall. Assume we are on linux. - # Allow the user to override what interface is used using an environment - # variable. - import fcntl, socket, struct, os - - iface = os.environ.get('VRM_IFACE', 'eth0').encode('ascii') - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - try: - info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', iface[:15])) - except IOError: - raise NoVrmPortalIdError("ioctl failed for eth0") - - __vrm_portal_id = info[18:24].hex() - return __vrm_portal_id - - -# See VE.Can registers - public.docx for definition of this conversion -def convert_vreg_version_to_readable(version): - def str_to_arr(x, length): - a = [] - for i in range(0, len(x), length): - a.append(x[i:i+length]) - return a - - x = "%x" % version - x = x.upper() - - if len(x) == 5 or len(x) == 3 or len(x) == 1: - x = '0' + x - - a = str_to_arr(x, 2); - - # remove the first 00 if there are three bytes and it is 00 - if len(a) == 3 and a[0] == '00': - a.remove(0); - - # if we have two or three bytes now, and the first character is a 0, remove it - if len(a) >= 2 and a[0][0:1] == '0': - a[0] = a[0][1]; - - result = '' - for item in a: - result += ('.' if result != '' else '') + item - - - result = 'v' + result - - return result - - -def get_free_space(path): - result = -1 - - try: - s = statvfs(path) - result = s.f_frsize * s.f_bavail # Number of free bytes that ordinary users - except Exception as ex: - logger.info("Error while retrieving free space for path %s: %s" % (path, ex)) - - return result - - -def _get_sysfs_machine_name(): - try: - with open('/sys/firmware/devicetree/base/model', 'r') as f: - return f.read().rstrip('\x00') - except IOError: - pass - - return None - -# Returns None if it cannot find a machine name. Otherwise returns the string -# containing the name -def get_machine_name(): - # First try calling the venus utility script - try: - return check_output("/usr/bin/product-name").strip().decode('UTF-8') - except (CalledProcessError, OSError): - pass - - # Fall back to sysfs - name = _get_sysfs_machine_name() - if name is not None: - return name - - # Fall back to venus build machine name - try: - with open('/etc/venus/machine', 'r', encoding='UTF-8') as f: - return f.read().strip() - except IOError: - pass - - return None - - -def get_product_id(): - """ Find the machine ID and return it. """ - - # First try calling the venus utility script - try: - return check_output("/usr/bin/product-id").strip().decode('UTF-8') - except (CalledProcessError, OSError): - pass - - # Fall back machine name mechanism - name = _get_sysfs_machine_name() - return { - 'Color Control GX': 'C001', - 'Venus GX': 'C002', - 'Octo GX': 'C006', - 'EasySolar-II': 'C007', - 'MultiPlus-II': 'C008', - 'Maxi GX': 'C009', - 'Cerbo GX': 'C00A' - }.get(name, 'C003') # C003 is Generic - - -# Returns False if it cannot open the file. Otherwise returns its rstripped contents -def read_file(path): - content = False - - try: - with open(path, 'r') as f: - content = f.read().rstrip() - except Exception as ex: - logger.debug("Error while reading %s: %s" % (path, ex)) - - return content - - -def wrap_dbus_value(value): - if value is None: - return VEDBUS_INVALID - if isinstance(value, float): - return dbus.Double(value, variant_level=1) - if isinstance(value, bool): - return dbus.Boolean(value, variant_level=1) - if isinstance(value, int): - try: - return dbus.Int32(value, variant_level=1) - except OverflowError: - return dbus.Int64(value, variant_level=1) - if isinstance(value, str): - return dbus.String(value, variant_level=1) - if isinstance(value, list): - if len(value) == 0: - # If the list is empty we cannot infer the type of the contents. So assume unsigned integer. - # A (signed) integer is dangerous, because an empty list of signed integers is used to encode - # an invalid value. - return dbus.Array([], signature=dbus.Signature('u'), variant_level=1) - return dbus.Array([wrap_dbus_value(x) for x in value], variant_level=1) - if isinstance(value, dict): - # Wrapping the keys of the dictionary causes D-Bus errors like: - # 'arguments to dbus_message_iter_open_container() were incorrect, - # assertion "(type == DBUS_TYPE_ARRAY && contained_signature && - # *contained_signature == DBUS_DICT_ENTRY_BEGIN_CHAR) || (contained_signature == NULL || - # _dbus_check_is_valid_signature (contained_signature))" failed in file ...' - return dbus.Dictionary({(k, wrap_dbus_value(v)) for k, v in value.items()}, variant_level=1) - return value - - -dbus_int_types = (dbus.Int32, dbus.UInt32, dbus.Byte, dbus.Int16, dbus.UInt16, dbus.UInt32, dbus.Int64, dbus.UInt64) - - -def unwrap_dbus_value(val): - """Converts D-Bus values back to the original type. For example if val is of type DBus.Double, - a float will be returned.""" - if isinstance(val, dbus_int_types): - return int(val) - if isinstance(val, dbus.Double): - return float(val) - if isinstance(val, dbus.Array): - v = [unwrap_dbus_value(x) for x in val] - return None if len(v) == 0 else v - if isinstance(val, (dbus.Signature, dbus.String)): - return str(val) - # Python has no byte type, so we convert to an integer. - if isinstance(val, dbus.Byte): - return int(val) - if isinstance(val, dbus.ByteArray): - return "".join([bytes(x) for x in val]) - if isinstance(val, (list, tuple)): - return [unwrap_dbus_value(x) for x in val] - if isinstance(val, (dbus.Dictionary, dict)): - # Do not unwrap the keys, see comment in wrap_dbus_value - return dict([(x, unwrap_dbus_value(y)) for x, y in val.items()]) - if isinstance(val, dbus.Boolean): - return bool(val) - return val - -# When supported, only name owner changes for the the given namespace are reported. This -# prevents spending cpu time at irrelevant changes, like scripts accessing the bus temporarily. -def add_name_owner_changed_receiver(dbus, name_owner_changed, namespace="com.victronenergy"): - # support for arg0namespace is submitted upstream, but not included at the time of - # writing, Venus OS does support it, so try if it works. - if namespace is None: - dbus.add_signal_receiver(name_owner_changed, signal_name='NameOwnerChanged') - else: - try: - dbus.add_signal_receiver(name_owner_changed, - signal_name='NameOwnerChanged', arg0namespace=namespace) - except TypeError: - dbus.add_signal_receiver(name_owner_changed, signal_name='NameOwnerChanged') diff --git a/velib_python/velib_python/latest/vedbus.py b/velib_python/velib_python/latest/vedbus.py deleted file mode 100644 index cb95ba1..0000000 --- a/velib_python/velib_python/latest/vedbus.py +++ /dev/null @@ -1,646 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import dbus.service -import logging -import traceback -import os -import weakref -from collections import defaultdict -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, register=True): - # dict containing the VeDbusItemExport objects, with their path as the key. - self._dbusobjects = {} - self._dbusnodes = {} - self._ratelimiters = [] - self._dbusname = None - self.name = servicename - - # 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 - - # Add the root item that will return all items as a tree - self._dbusnodes['/'] = VeDbusRootExport(self._dbusconn, '/', self) - - # Immediately register the service unless requested not to - if register: - self.register() - - def register(self): - # Register ourselves on the dbus, trigger an error if already in use (do_not_queue) - self._dbusname = dbus.service.BusName(self.name, self._dbusconn, do_not_queue=True) - logging.info("registered ourselves on D-Bus as %s" % self.name) - - # To force immediate deregistering of this dbus service and all its object paths, explicitly - # call __del__(). - def __del__(self): - for node in list(self._dbusnodes.values()): - node.__del__() - self._dbusnodes.clear() - for item in list(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 - - def get_name(self): - return self._dbusname.get_name() - - # @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, valuetype=None, itemtype=None): - - if onchangecallback is not None: - self._onchangecallbacks[path] = onchangecallback - - itemtype = itemtype or VeDbusItemExport - item = itemtype(self._dbusconn, path, value, description, writeable, - self._value_changed, gettextcallback, deletecallback=self._item_deleted, valuetype=valuetype) - - 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] = VeDbusTreeExport(self._dbusconn, subPath, self) - self._dbusobjects[path] = item - logging.debug('added %s with start value %s. Writeable is %s' % (path, value, writeable)) - return item - - # 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) - - # 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 list(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 - - def __enter__(self): - l = ServiceContext(self) - self._ratelimiters.append(l) - return l - - def __exit__(self, *exc): - # pop off the top one and flush it. If with statements are nested - # then each exit flushes its own part. - if self._ratelimiters: - self._ratelimiters.pop().flush() - -class ServiceContext(object): - def __init__(self, parent): - self.parent = parent - self.changes = {} - - def __contains__(self, path): - return path in self.parent - - def __getitem__(self, path): - return self.parent[path] - - def __setitem__(self, path, newvalue): - c = self.parent._dbusobjects[path]._local_set_value(newvalue) - if c is not None: - self.changes[path] = c - - def __delitem__(self, path): - if path in self.changes: - del self.changes[path] - del self.parent[path] - - def flush(self): - if self.changes: - self.parent._dbusnodes['/'].ItemsChanged(self.changes) - self.changes.clear() - - def add_path(self, path, value, *args, **kwargs): - self.parent.add_path(path, value, *args, **kwargs) - self.changes[path] = { - 'Value': wrap_dbus_value(value), - 'Text': self.parent._dbusobjects[path].GetText() - } - - def del_tree(self, root): - root = root.rstrip('/') - for p in list(self.parent._dbusobjects.keys()): - if p == root or p.startswith(root + '/'): - self[p] = None - self.parent._dbusobjects[p].__del__() - - def get_name(self): - return self.parent.get_name() - -class TrackerDict(defaultdict): - """ Same as defaultdict, but passes the key to default_factory. """ - def __missing__(self, key): - self[key] = x = self.default_factory(key) - return x - -class VeDbusRootTracker(object): - """ This tracks the root of a dbus path and listens for PropertiesChanged - signals. When a signal arrives, parse it and unpack the key/value changes - into traditional events, then pass it to the original eventCallback - method. """ - def __init__(self, bus, serviceName): - self.importers = defaultdict(weakref.WeakSet) - self.serviceName = serviceName - self._match = bus.get_object(serviceName, '/', introspect=False).connect_to_signal( - "ItemsChanged", weak_functor(self._items_changed_handler)) - - def __del__(self): - self._match.remove() - self._match = None - - def add(self, i): - self.importers[i.path].add(i) - - def _items_changed_handler(self, items): - if not isinstance(items, dict): - return - - for path, changes in items.items(): - try: - v = changes['Value'] - except KeyError: - continue - - try: - t = changes['Text'] - except KeyError: - t = str(unwrap_dbus_value(v)) - - for i in self.importers.get(path, ()): - i._properties_changed_handler({'Value': v, 'Text': t}) - -""" -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): - def __new__(cls, bus, serviceName, path, eventCallback=None, createsignal=True): - instance = object.__new__(cls) - - # If signal tracking should be done, also add to root tracker - if createsignal: - if "_roots" not in cls.__dict__: - cls._roots = TrackerDict(lambda k: VeDbusRootTracker(bus, k)) - - return instance - - ## 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)) - self._roots[serviceName].add(self) - - # 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 is not 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 - - ## Resets the item to its default value - def set_default(self): - self._proxy.SetDefault() - self._refreshcachedvalue() - - ## 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, service): - dbus.service.Object.__init__(self, bus, objectPath) - self._service = service - 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] - - def _get_value_handler(self, path, get_text=False): - logging.debug("_get_value_handler called for %s" % path) - r = {} - px = path - if not px.endswith('/'): - px += '/' - for p, item in self._service._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 - - @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 VeDbusRootExport(VeDbusTreeExport): - @dbus.service.signal('com.victronenergy.BusItem', signature='a{sa{sv}}') - def ItemsChanged(self, changes): - pass - - @dbus.service.method('com.victronenergy.BusItem', out_signature='a{sa{sv}}') - def GetItems(self): - return { - path: { - 'Value': wrap_dbus_value(item.local_get_value()), - 'Text': item.GetText() } - for path, item in self._service._dbusobjects.items() - } - - -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, - valuetype=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 - self._type = valuetype - - # 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.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): - changes = self._local_set_value(newvalue) - if changes is not None: - self.PropertiesChanged(changes) - - def _local_set_value(self, newvalue): - if self._value == newvalue: - return None - - self._value = newvalue - return { - 'Value': wrap_dbus_value(newvalue), - 'Text': self.GetText() - } - - 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 value type is enforced, cast it. If the type can be coerced - # python will do it for us. This allows ints to become floats, - # or bools to become ints. Additionally also allow None, so that - # a path may be invalidated. - if self._type is not None and newvalue is not None: - try: - newvalue = self._type(newvalue) - except (ValueError, TypeError): - return 1 # NOT OK - - 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) diff --git a/velib_python/velib_python/v2.73/dbusmonitor.py b/velib_python/velib_python/v2.73/dbusmonitor.py deleted file mode 100644 index fc8785d..0000000 --- a/velib_python/velib_python/v2.73/dbusmonitor.py +++ /dev/null @@ -1,535 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -## @package dbus_vrm -# This code takes care of the D-Bus interface (not all of below is implemented yet): -# - on startup it scans the dbus for services we know. For each known service found, it searches for -# objects/paths we know. Everything we find is stored in items{}, and an event is registered: if a -# value changes weĺl be notified and can pass that on to our owner. For example the vrmLogger. -# we know. -# - after startup, it continues to monitor the dbus: -# 1) when services are added we do the same check on that -# 2) when services are removed, we remove any items that we had that referred to that service -# 3) if an existing services adds paths we update ourselves as well: on init, we make a -# VeDbusItemImport for a non-, or not yet existing objectpaths as well1 -# -# Code is used by the vrmLogger, and also the pubsub code. Both are other modules in the dbus_vrm repo. - -from dbus.mainloop.glib import DBusGMainLoop -import gobject -from gobject import idle_add -import dbus -import dbus.service -import inspect -import logging -import argparse -import pprint -import traceback -import os -from collections import defaultdict -from functools import partial - -# our own packages -from vedbus import VeDbusItemExport, VeDbusItemImport -from ve_utils import exit_on_error, wrap_dbus_value, unwrap_dbus_value -notfound = object() # For lookups where None is a valid result - -logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) -class SystemBus(dbus.bus.BusConnection): - def __new__(cls): - return dbus.bus.BusConnection.__new__(cls, dbus.bus.BusConnection.TYPE_SYSTEM) - -class SessionBus(dbus.bus.BusConnection): - def __new__(cls): - return dbus.bus.BusConnection.__new__(cls, dbus.bus.BusConnection.TYPE_SESSION) - -class MonitoredValue(object): - def __init__(self, value, text, options): - super(MonitoredValue, self).__init__() - self.value = value - self.text = text - self.options = options - - # For legacy code, allow treating this as a tuple/list - def __iter__(self): - return iter((self.value, self.text, self.options)) - -class Service(object): - whentologoptions = ['configChange', 'onIntervalAlwaysAndOnEvent', - 'onIntervalOnlyWhenChanged', 'onIntervalAlways', 'never'] - def __init__(self, id, serviceName, deviceInstance): - super(Service, self).__init__() - self.id = id - self.name = serviceName - self.paths = {} - self._seen = set() - self.deviceInstance = deviceInstance - - self.configChange = [] - self.onIntervalAlwaysAndOnEvent = [] - self.onIntervalOnlyWhenChanged = [] - self.onIntervalAlways = [] - self.never = [] - - # For legacy code, attributes can still be accessed as if keys from a - # dictionary. - def __setitem__(self, key, value): - self.__dict__[key] = value - def __getitem__(self, key): - return self.__dict__[key] - - def set_seen(self, path): - self._seen.add(path) - - def seen(self, path): - return path in self._seen - - @property - def service_class(self): - return '.'.join(self.name.split('.')[:3]) - -class DbusMonitor(object): - ## Constructor - def __init__(self, dbusTree, valueChangedCallback=None, deviceAddedCallback=None, - deviceRemovedCallback=None, vebusDeviceInstance0=False): - # valueChangedCallback is the callback that we call when something has changed. - # def value_changed_on_dbus(dbusServiceName, dbusPath, options, changes, deviceInstance): - # in which changes is a tuple with GetText() and GetValue() - self.valueChangedCallback = valueChangedCallback - self.deviceAddedCallback = deviceAddedCallback - self.deviceRemovedCallback = deviceRemovedCallback - self.dbusTree = dbusTree - self.vebusDeviceInstance0 = vebusDeviceInstance0 - - # Lists all tracked services. Stores name, id, device instance, value per path, and whenToLog info - # indexed by service name (eg. com.victronenergy.settings). - self.servicesByName = {} - - # Same values as self.servicesByName, but indexed by service id (eg. :1.30) - self.servicesById = {} - - # Keep track of services by class to speed up calls to get_service_list - self.servicesByClass = defaultdict(list) - - # Keep track of any additional watches placed on items - self.serviceWatches = defaultdict(list) - - # For a PC, connect to the SessionBus - # For a CCGX, connect to the SystemBus - self.dbusConn = SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else SystemBus() - - # subscribe to NameOwnerChange for bus connect / disconnect events. - (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ \ - else dbus.SystemBus()).add_signal_receiver( - self.dbus_name_owner_changed, - signal_name='NameOwnerChanged') - - # Subscribe to PropertiesChanged for all services - self.dbusConn.add_signal_receiver(self.handler_value_changes, - dbus_interface='com.victronenergy.BusItem', - signal_name='PropertiesChanged', path_keyword='path', - sender_keyword='senderId') - - logger.info('===== Search on dbus for services that we will monitor starting... =====') - serviceNames = self.dbusConn.list_names() - for serviceName in serviceNames: - self.scan_dbus_service(serviceName) - - logger.info('===== Search on dbus for services that we will monitor finished =====') - - def dbus_name_owner_changed(self, name, oldowner, newowner): - if not name.startswith("com.victronenergy."): - return - - #decouple, and process in main loop - idle_add(exit_on_error, self._process_name_owner_changed, name, oldowner, newowner) - - def _process_name_owner_changed(self, name, oldowner, newowner): - if newowner != '': - # so we found some new service. Check if we can do something with it. - newdeviceadded = self.scan_dbus_service(name) - if newdeviceadded and self.deviceAddedCallback is not None: - self.deviceAddedCallback(name, self.get_device_instance(name)) - - elif name in self.servicesByName: - # it disappeared, we need to remove it. - logger.info("%s disappeared from the dbus. Removing it from our lists" % name) - service = self.servicesByName[name] - deviceInstance = service['deviceInstance'] - del self.servicesById[service.id] - del self.servicesByName[name] - for watch in self.serviceWatches[name]: - watch.remove() - del self.serviceWatches[name] - self.servicesByClass[service.service_class].remove(service) - if self.deviceRemovedCallback is not None: - self.deviceRemovedCallback(name, deviceInstance) - - def scan_dbus_service(self, serviceName): - try: - return self.scan_dbus_service_inner(serviceName) - except: - logger.error("Ignoring %s because of error while scanning:" % (serviceName)) - traceback.print_exc() - return False - - # Errors 'org.freedesktop.DBus.Error.ServiceUnknown' and - # 'org.freedesktop.DBus.Error.Disconnected' seem to happen when the service - # disappears while its being scanned. Which might happen, but is not really - # normal either, so letting them go into the logs. - - # Scans the given dbus service to see if it contains anything interesting for us. If it does, add - # it to our list of monitored D-Bus services. - def scan_dbus_service_inner(self, serviceName): - - # make it a normal string instead of dbus string - serviceName = str(serviceName) - - paths = self.dbusTree.get('.'.join(serviceName.split('.')[0:3]), None) - if paths is None: - logger.debug("Ignoring service %s, not in the tree" % serviceName) - return False - - logger.info("Found: %s, scanning and storing items" % serviceName) - serviceId = self.dbusConn.get_name_owner(serviceName) - - # we should never be notified to add a D-Bus service that we already have. If this assertion - # raises, check process_name_owner_changed, and D-Bus workings. - assert serviceName not in self.servicesByName - assert serviceId not in self.servicesById - - # for vebus.ttyO1, this is workaround, since VRM Portal expects the main vebus - # devices at instance 0. Not sure how to fix this yet. - if serviceName == 'com.victronenergy.vebus.ttyO1' and self.vebusDeviceInstance0: - di = 0 - elif serviceName == 'com.victronenergy.settings': - di = 0 - elif serviceName.startswith('com.victronenergy.vecan.'): - di = 0 - else: - try: - di = self.dbusConn.call_blocking(serviceName, - '/DeviceInstance', None, 'GetValue', '', []) - except dbus.exceptions.DBusException: - logger.info(" %s was skipped because it has no device instance" % serviceName) - return False # Skip it - else: - di = int(di) - - logger.info(" %s has device instance %s" % (serviceName, di)) - service = Service(serviceId, serviceName, di) - - # Let's try to fetch everything in one go - values = {} - texts = {} - try: - values.update(self.dbusConn.call_blocking(serviceName, '/', None, 'GetValue', '', [])) - texts.update(self.dbusConn.call_blocking(serviceName, '/', None, 'GetText', '', [])) - except: - pass - - for path, options in paths.iteritems(): - # path will be the D-Bus path: '/Ac/ActiveIn/L1/V' - # options will be a dictionary: {'code': 'V', 'whenToLog': 'onIntervalAlways'} - # check that the whenToLog setting is set to something we expect - assert options['whenToLog'] is None or options['whenToLog'] in Service.whentologoptions - - # Try to obtain the value we want from our bulk fetch. If we - # cannot find it there, do an individual query. - value = values.get(path[1:], notfound) - if value != notfound: - service.set_seen(path) - text = texts.get(path[1:], notfound) - if value is notfound or text is notfound: - try: - value = self.dbusConn.call_blocking(serviceName, path, None, 'GetValue', '', []) - service.set_seen(path) - text = self.dbusConn.call_blocking(serviceName, path, None, 'GetText', '', []) - except dbus.exceptions.DBusException as e: - if e.get_dbus_name() in ( - 'org.freedesktop.DBus.Error.ServiceUnknown', - 'org.freedesktop.DBus.Error.Disconnected'): - raise # This exception will be handled below - - # TODO org.freedesktop.DBus.Error.UnknownMethod really - # shouldn't happen but sometimes does. - logger.debug("%s %s does not exist (yet)" % (serviceName, path)) - value = None - text = None - - service.paths[path] = MonitoredValue(unwrap_dbus_value(value), unwrap_dbus_value(text), options) - - if options['whenToLog']: - service[options['whenToLog']].append(path) - - - logger.debug("Finished scanning and storing items for %s" % serviceName) - - # Adjust self at the end of the scan, so we don't have an incomplete set of - # data if an exception occurs during the scan. - self.servicesByName[serviceName] = service - self.servicesById[serviceId] = service - self.servicesByClass[service.service_class].append(service) - - return True - - def handler_value_changes(self, changes, path, senderId): - try: - service = self.servicesById[senderId] - a = service.paths[path] - except KeyError: - # Either senderId or path isn't there, which means - # it hasn't been scanned yet. - return - - # If this properyChange does not involve a value, our work is done. - if 'Value' not in changes: - return - - service.set_seen(path) - - # First update our store to the new value - changes['Value'] = unwrap_dbus_value(changes['Value']) - if a.value == changes['Value']: - return - - a.value = changes['Value'] - try: - a.text = changes['Text'] - except KeyError: - # Some services don't send Text with their PropertiesChanged events. - a.text = str(a.value) - - # And do the rest of the processing in on the mainloop - if self.valueChangedCallback is not None: - idle_add(exit_on_error, self._execute_value_changes, service.name, path, changes, a.options) - - def _execute_value_changes(self, serviceName, objectPath, changes, options): - # double check that the service still exists, as it might have - # disappeared between scheduling-for and executing this function. - if serviceName not in self.servicesByName: - return - - self.valueChangedCallback(serviceName, objectPath, - options, changes, self.get_device_instance(serviceName)) - - # Gets the value for a certain servicename and path - # The default_value is returned when: - # 1. When the service doesn't exist. - # 2. When the path asked for isn't being monitored. - # 3. When the path exists, but has dbus-invalid, ie an empty byte array. - # 4. When the path asked for is being monitored, but doesn't exist for that service. - def get_value(self, serviceName, objectPath, default_value=None): - service = self.servicesByName.get(serviceName, None) - if service is None: - return default_value - - value = service.paths.get(objectPath, None) - if value is None or value.value is None: - return default_value - - return value.value - - # returns if a dbus exists now, by doing a blocking dbus call. - # Typically seen will be sufficient and doesn't need access to the dbus. - def exists(self, serviceName, objectPath): - try: - self.dbusConn.call_blocking(serviceName, objectPath, None, 'GetValue', '', []) - return True - except dbus.exceptions.DBusException as e: - return False - - # Returns if there ever was a successful GetValue or valueChanged event. - # Unlike get_value this return True also if the actual value is invalid. - # - # Note: the path might no longer exists anymore, but that doesn't happen in - # practice. If a service really wants to reconfigure itself typically it should - # reconnect to the dbus which causes it to be rescanned and seen will be updated. - # If it is really needed to know if a path still exists, use exists. - def seen(self, serviceName, objectPath): - try: - return self.servicesByName[serviceName].seen(objectPath) - except KeyError: - return False - - # Sets the value for a certain servicename and path, returns the return value of the D-Bus SetValue - # method. If the underlying item does not exist (the service does not exist, or the objectPath was not - # registered) the function will return -1 - def set_value(self, serviceName, objectPath, value): - # Check if the D-Bus object referenced by serviceName and objectPath is registered. There is no - # necessity to do this, but it is in line with previous implementations which kept VeDbusItemImport - # objects for registers items only. - service = self.servicesByName.get(serviceName, None) - if service is None: - return -1 - if objectPath not in service.paths: - return -1 - # We do not catch D-Bus exceptions here, because the previous implementation did not do that either. - return self.dbusConn.call_blocking(serviceName, objectPath, - dbus_interface='com.victronenergy.BusItem', - method='SetValue', signature=None, - args=[wrap_dbus_value(value)]) - - # Similar to set_value, but operates asynchronously - def set_value_async(self, serviceName, objectPath, value, - reply_handler=None, error_handler=None): - service = self.servicesByName.get(serviceName, None) - if service is not None: - if objectPath in service.paths: - self.dbusConn.call_async(serviceName, objectPath, - dbus_interface='com.victronenergy.BusItem', - method='SetValue', signature=None, - args=[wrap_dbus_value(value)], - reply_handler=reply_handler, error_handler=error_handler) - return - - if error_handler is not None: - error_handler(TypeError('Service or path not found, ' - 'service=%s, path=%s' % (serviceName, objectPath))) - - # returns a dictionary, keys are the servicenames, value the instances - # optionally use the classfilter to get only a certain type of services, for - # example com.victronenergy.battery. - def get_service_list(self, classfilter=None): - if classfilter is None: - return { servicename: service.deviceInstance \ - for servicename, service in self.servicesByName.iteritems() } - - if classfilter not in self.servicesByClass: - return {} - - return { service.name: service.deviceInstance \ - for service in self.servicesByClass[classfilter] } - - def get_device_instance(self, serviceName): - return self.servicesByName[serviceName].deviceInstance - - # Parameter categoryfilter is to be a list, containing the categories you want (configChange, - # onIntervalAlways, etc). - # Returns a dictionary, keys are codes + instance, in VRM querystring format. For example vvt[0]. And - # values are the value. - def get_values(self, categoryfilter, converter=None): - - result = {} - - for serviceName in self.servicesByName: - result.update(self.get_values_for_service(categoryfilter, serviceName, converter)) - - return result - - # same as get_values above, but then for one service only - def get_values_for_service(self, categoryfilter, servicename, converter=None): - deviceInstance = self.get_device_instance(servicename) - result = {} - - service = self.servicesByName[servicename] - - for category in categoryfilter: - - for path in service[category]: - - value, text, options = service.paths[path] - - if value is not None: - - value = value if converter is None else converter.convert(path, options['code'], value, text) - - precision = options.get('precision') - if precision: - value = round(value, precision) - - result[options['code'] + "[" + str(deviceInstance) + "]"] = value - - return result - - def track_value(self, serviceName, objectPath, callback, *args, **kwargs): - """ A DbusMonitor can watch specific service/path combos for changes - so that it is not fully reliant on the global handler_value_changes - in this class. Additional watches are deleted automatically when - the service disappears from dbus. """ - self.serviceWatches[serviceName].append( - self.dbusConn.add_signal_receiver( - partial(callback, *args, **kwargs), - dbus_interface='com.victronenergy.BusItem', - signal_name='PropertiesChanged', - path=objectPath, bus_name=serviceName)) - - -# ====== ALL CODE BELOW THIS LINE IS PURELY FOR DEVELOPING THIS CLASS ====== - -# Example function that can be used as a starting point to use this code -def value_changed_on_dbus(dbusServiceName, dbusPath, dict, changes, deviceInstance): - logger.debug("0 ----------------") - logger.debug("1 %s%s changed" % (dbusServiceName, dbusPath)) - logger.debug("2 vrm dict : %s" % dict) - logger.debug("3 changes-text: %s" % changes['Text']) - logger.debug("4 changes-value: %s" % changes['Value']) - logger.debug("5 deviceInstance: %s" % deviceInstance) - logger.debug("6 - end") - - -def nameownerchange(a, b): - # used to find memory leaks in dbusmonitor and VeDbusItemImport - import gc - gc.collect() - objects = gc.get_objects() - print len([o for o in objects if type(o).__name__ == 'VeDbusItemImport']) - print len([o for o in objects if type(o).__name__ == 'SignalMatch']) - print len(objects) - - -def print_values(dbusmonitor): - a = dbusmonitor.get_value('wrongservice', '/DbusInvalid', default_value=1000) - b = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/NotInTheMonitorList', default_value=1000) - c = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/DbusInvalid', default_value=1000) - d = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/NonExistingButMonitored', default_value=1000) - - print "All should be 1000: Wrong Service: %s, NotInTheMonitorList: %s, DbusInvalid: %s, NonExistingButMonitored: %s" % (a, b, c, d) - return True - -# We have a mainloop, but that is just for developing this code. Normally above class & code is used from -# some other class, such as vrmLogger or the pubsub Implementation. -def main(): - # Init logging - logging.basicConfig(level=logging.DEBUG) - logger.info(__file__ + " is starting up") - - # Have a mainloop, so we can send/receive asynchronous calls to and from dbus - DBusGMainLoop(set_as_default=True) - - import os - import sys - sys.path.insert(1, os.path.join(os.path.dirname(__file__), '../../')) - - dummy = {'code': None, 'whenToLog': 'configChange', 'accessLevel': None} - monitorlist = {'com.victronenergy.dummyservice': { - '/Connected': dummy, - '/ProductName': dummy, - '/Mgmt/Connection': dummy, - '/Dc/0/Voltage': dummy, - '/Dc/0/Current': dummy, - '/Dc/0/Temperature': dummy, - '/Load/I': dummy, - '/FirmwareVersion': dummy, - '/DbusInvalid': dummy, - '/NonExistingButMonitored': dummy}} - - d = DbusMonitor(monitorlist, value_changed_on_dbus, - deviceAddedCallback=nameownerchange, deviceRemovedCallback=nameownerchange) - - # logger.info("==configchange values==") - # logger.info(pprint.pformat(d.get_values(['configChange']))) - - # logger.info("==onIntervalAlways and onIntervalOnlyWhenChanged==") - # logger.info(pprint.pformat(d.get_values(['onIntervalAlways', 'onIntervalAlwaysAndOnEvent']))) - - gobject.timeout_add(1000, print_values, d) - - # Start and run the mainloop - logger.info("Starting mainloop, responding on only events") - mainloop = gobject.MainLoop() - mainloop.run() - -if __name__ == "__main__": - main() diff --git a/velib_python/velib_python/v2.73/oldestVersion b/velib_python/velib_python/v2.73/oldestVersion deleted file mode 100644 index c4da1d4..0000000 --- a/velib_python/velib_python/v2.73/oldestVersion +++ /dev/null @@ -1 +0,0 @@ -v2.71 diff --git a/velib_python/velib_python/v2.73/settingsdevice.py b/velib_python/velib_python/v2.73/settingsdevice.py deleted file mode 100644 index b525375..0000000 --- a/velib_python/velib_python/v2.73/settingsdevice.py +++ /dev/null @@ -1,115 +0,0 @@ -import dbus -import logging -import time -from functools import partial - -# Local imports -from vedbus import VeDbusItemImport - -## Indexes for the setting dictonary. -PATH = 0 -VALUE = 1 -MINIMUM = 2 -MAXIMUM = 3 -SILENT = 4 - -## The Settings Device class. -# Used by python programs, such as the vrm-logger, to read and write settings they -# need to store on disk. And since these settings might be changed from a different -# source, such as the GUI, the program can pass an eventCallback that will be called -# as soon as some setting is changed. -# -# The settings are stored in flash via the com.victronenergy.settings service on dbus. -# See https://github.com/victronenergy/localsettings for more info. -# -# If there are settings in de supportSettings list which are not yet on the dbus, -# and therefore not yet in the xml file, they will be added through the dbus-addSetting -# interface of com.victronenergy.settings. -class SettingsDevice(object): - ## The constructor processes the tree of dbus-items. - # @param bus the system-dbus object - # @param name the dbus-service-name of the settings dbus service, 'com.victronenergy.settings' - # @param supportedSettings dictionary with all setting-names, and their defaultvalue, min, max and whether - # the setting is silent. The 'silent' entry is optional. If set to true, no changes in the setting will - # be logged by localsettings. - # @param eventCallback function that will be called on changes on any of these settings - # @param timeout Maximum interval to wait for localsettings. An exception is thrown at the end of the - # interval if the localsettings D-Bus service has not appeared yet. - def __init__(self, bus, supportedSettings, eventCallback, name='com.victronenergy.settings', timeout=0): - logging.debug("===== Settings device init starting... =====") - self._bus = bus - self._dbus_name = name - self._eventCallback = eventCallback - self._values = {} # stored the values, used to pass the old value along on a setting change - self._settings = {} - - count = 0 - while True: - if 'com.victronenergy.settings' in self._bus.list_names(): - break - if count == timeout: - raise Exception("The settings service com.victronenergy.settings does not exist!") - count += 1 - logging.info('waiting for settings') - time.sleep(1) - - # Add the items. - for setting, options in supportedSettings.items(): - silent = len(options) > SILENT and options[SILENT] - busitem = self.addSetting(options[PATH], options[VALUE], - options[MINIMUM], options[MAXIMUM], silent, callback=partial(self.handleChangedSetting, setting)) - self._settings[setting] = busitem - self._values[setting] = busitem.get_value() - - logging.debug("===== Settings device init finished =====") - - def addSetting(self, path, value, _min, _max, silent=False, callback=None): - busitem = VeDbusItemImport(self._bus, self._dbus_name, path, callback) - if busitem.exists and (value, _min, _max, silent) == busitem._proxy.GetAttributes(): - logging.debug("Setting %s found" % path) - else: - logging.info("Setting %s does not exist yet or must be adjusted" % path) - - # Prepare to add the setting. Most dbus types extend the python - # type so it is only necessary to additionally test for Int64. - if isinstance(value, (int, dbus.Int64)): - itemType = 'i' - elif isinstance(value, float): - itemType = 'f' - else: - itemType = 's' - - # Add the setting - # TODO, make an object that inherits VeDbusItemImport, and complete the D-Bus settingsitem interface - settings_item = VeDbusItemImport(self._bus, self._dbus_name, '/Settings', createsignal=False) - setting_path = path.replace('/Settings/', '', 1) - if silent: - settings_item._proxy.AddSilentSetting('', setting_path, value, itemType, _min, _max) - else: - settings_item._proxy.AddSetting('', setting_path, value, itemType, _min, _max) - - busitem = VeDbusItemImport(self._bus, self._dbus_name, path, callback) - - return busitem - - def handleChangedSetting(self, setting, servicename, path, changes): - oldvalue = self._values[setting] if setting in self._values else None - self._values[setting] = changes['Value'] - - if self._eventCallback is None: - return - - self._eventCallback(setting, oldvalue, changes['Value']) - - def setDefault(self, path): - item = VeDbusItemImport(self._bus, self._dbus_name, path, createsignal=False) - item.set_default() - - def __getitem__(self, setting): - return self._settings[setting].get_value() - - def __setitem__(self, setting, newvalue): - result = self._settings[setting].set_value(newvalue) - if result != 0: - # Trying to make some false change to our own settings? How dumb! - assert False diff --git a/velib_python/velib_python/v2.73/ve_utils.py b/velib_python/velib_python/v2.73/ve_utils.py deleted file mode 100644 index c5cfb74..0000000 --- a/velib_python/velib_python/v2.73/ve_utils.py +++ /dev/null @@ -1,253 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -from traceback import print_exc -from os import _exit as os_exit -from os import statvfs -from subprocess import check_output, CalledProcessError -import logging -import dbus -logger = logging.getLogger(__name__) - -VEDBUS_INVALID = dbus.Array([], signature=dbus.Signature('i'), variant_level=1) - -# Use this function to make sure the code quits on an unexpected exception. Make sure to use it -# when using gobject.idle_add and also gobject.timeout_add. -# Without this, the code will just keep running, since gobject does not stop the mainloop on an -# exception. -# Example: gobject.idle_add(exit_on_error, myfunc, arg1, arg2) -def exit_on_error(func, *args, **kwargs): - try: - return func(*args, **kwargs) - except: - try: - print 'exit_on_error: there was an exception. Printing stacktrace will be tried and then exit' - print_exc() - except: - pass - - # sys.exit() is not used, since that throws an exception, which does not lead to a program - # halt when used in a dbus callback, see connection.py in the Python/Dbus libraries, line 230. - os_exit(1) - - -__vrm_portal_id = None -def get_vrm_portal_id(): - # For the CCGX, the definition of the VRM Portal ID is that it is the mac address of the onboard- - # ethernet port (eth0), stripped from its colons (:) and lower case. - - # nice coincidence is that this also works fine when running on your (linux) development computer. - - global __vrm_portal_id - - if __vrm_portal_id: - return __vrm_portal_id - - # First try the method that works if we don't have a data partition. This will fail - # when the current user is not root. - try: - __vrm_portal_id = check_output("/sbin/get-unique-id").strip() - return __vrm_portal_id - except (CalledProcessError, OSError): - pass - - # Attempt to get the id from /data/venus/unique-id where venus puts it - # on startup. - try: - __vrm_portal_id = open('/data/venus/unique-id').read().strip() - except IOError: - pass - else: - return __vrm_portal_id - - # Fall back to getting our id using a syscall. Assume we are on linux. - # Allow the user to override what interface is used using an environment - # variable. - import fcntl, socket, struct, os - - iface = os.environ.get('VRM_IFACE', 'eth0') - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', iface[:15])) - __vrm_portal_id = ''.join(['%02x' % ord(char) for char in info[18:24]]) - return __vrm_portal_id - - -# See VE.Can registers - public.docx for definition of this conversion -def convert_vreg_version_to_readable(version): - def str_to_arr(x, length): - a = [] - for i in range(0, len(x), length): - a.append(x[i:i+length]) - return a - - x = "%x" % version - x = x.upper() - - if len(x) == 5 or len(x) == 3 or len(x) == 1: - x = '0' + x - - a = str_to_arr(x, 2); - - # remove the first 00 if there are three bytes and it is 00 - if len(a) == 3 and a[0] == '00': - a.remove(0); - - # if we have two or three bytes now, and the first character is a 0, remove it - if len(a) >= 2 and a[0][0:1] == '0': - a[0] = a[0][1]; - - result = '' - for item in a: - result += ('.' if result != '' else '') + item - - - result = 'v' + result - - return result - - -def get_free_space(path): - result = -1 - - try: - s = statvfs(path) - result = s.f_frsize * s.f_bavail # Number of free bytes that ordinary users - except Exception, ex: - logger.info("Error while retrieving free space for path %s: %s" % (path, ex)) - - return result - - -def get_load_averages(): - c = read_file('/proc/loadavg') - return c.split(' ')[:3] - - -def _get_sysfs_machine_name(): - try: - with open('/sys/firmware/devicetree/base/model', 'r') as f: - return f.read().rstrip('\x00') - except IOError: - pass - - return None - -# Returns None if it cannot find a machine name. Otherwise returns the string -# containing the name -def get_machine_name(): - # First try calling the venus utility script - try: - return check_output("/usr/bin/product-name").strip() - except (CalledProcessError, OSError): - pass - - # Fall back to sysfs - name = _get_sysfs_machine_name() - if name is not None: - return name - - # Fall back to venus build machine name - try: - with open('/etc/venus/machine', 'r') as f: - return f.read().strip() - except IOError: - pass - - return None - - -def get_product_id(): - """ Find the machine ID and return it. """ - - # First try calling the venus utility script - try: - return check_output("/usr/bin/product-id").strip() - except (CalledProcessError, OSError): - pass - - # Fall back machine name mechanism - name = _get_sysfs_machine_name() - return { - 'Color Control GX': 'C001', - 'Venus GX': 'C002', - 'Octo GX': 'C006', - 'EasySolar-II': 'C007', - 'MultiPlus-II': 'C008' - }.get(name, 'C003') # C003 is Generic - - -# Returns False if it cannot open the file. Otherwise returns its rstripped contents -def read_file(path): - content = False - - try: - with open(path, 'r') as f: - content = f.read().rstrip() - except Exception, ex: - logger.debug("Error while reading %s: %s" % (path, ex)) - - return content - - -def wrap_dbus_value(value): - if value is None: - return VEDBUS_INVALID - if isinstance(value, float): - return dbus.Double(value, variant_level=1) - if isinstance(value, bool): - return dbus.Boolean(value, variant_level=1) - if isinstance(value, int): - try: - return dbus.Int32(value, variant_level=1) - except OverflowError: - return dbus.Int64(value, variant_level=1) - if isinstance(value, str): - return dbus.String(value, variant_level=1) - if isinstance(value, unicode): - return dbus.String(value, variant_level=1) - if isinstance(value, list): - if len(value) == 0: - # If the list is empty we cannot infer the type of the contents. So assume unsigned integer. - # A (signed) integer is dangerous, because an empty list of signed integers is used to encode - # an invalid value. - return dbus.Array([], signature=dbus.Signature('u'), variant_level=1) - return dbus.Array([wrap_dbus_value(x) for x in value], variant_level=1) - if isinstance(value, long): - return dbus.Int64(value, variant_level=1) - if isinstance(value, dict): - # Wrapping the keys of the dictionary causes D-Bus errors like: - # 'arguments to dbus_message_iter_open_container() were incorrect, - # assertion "(type == DBUS_TYPE_ARRAY && contained_signature && - # *contained_signature == DBUS_DICT_ENTRY_BEGIN_CHAR) || (contained_signature == NULL || - # _dbus_check_is_valid_signature (contained_signature))" failed in file ...' - return dbus.Dictionary({(k, wrap_dbus_value(v)) for k, v in value.items()}, variant_level=1) - return value - - -dbus_int_types = (dbus.Int32, dbus.UInt32, dbus.Byte, dbus.Int16, dbus.UInt16, dbus.UInt32, dbus.Int64, dbus.UInt64) - - -def unwrap_dbus_value(val): - """Converts D-Bus values back to the original type. For example if val is of type DBus.Double, - a float will be returned.""" - if isinstance(val, dbus_int_types): - return int(val) - if isinstance(val, dbus.Double): - return float(val) - if isinstance(val, dbus.Array): - v = [unwrap_dbus_value(x) for x in val] - return None if len(v) == 0 else v - if isinstance(val, (dbus.Signature, dbus.String)): - return unicode(val) - # Python has no byte type, so we convert to an integer. - if isinstance(val, dbus.Byte): - return int(val) - if isinstance(val, dbus.ByteArray): - return "".join([str(x) for x in val]) - if isinstance(val, (list, tuple)): - return [unwrap_dbus_value(x) for x in val] - if isinstance(val, (dbus.Dictionary, dict)): - # Do not unwrap the keys, see comment in wrap_dbus_value - return dict([(x, unwrap_dbus_value(y)) for x, y in val.items()]) - if isinstance(val, dbus.Boolean): - return bool(val) - return val diff --git a/velib_python/velib_python/v2.73/vedbus.py b/velib_python/velib_python/v2.73/vedbus.py deleted file mode 100644 index 7fbe55d..0000000 --- a/velib_python/velib_python/v2.73/vedbus.py +++ /dev/null @@ -1,501 +0,0 @@ -#!/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 - - ## Resets the item to its default value - def set_default(self): - self._proxy.SetDefault() - self._refreshcachedvalue() - - ## 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) diff --git a/velib_python/velib_python/v2.94/dbusmonitor.py b/velib_python/velib_python/v2.94/dbusmonitor.py deleted file mode 100644 index 5f8e153..0000000 --- a/velib_python/velib_python/v2.94/dbusmonitor.py +++ /dev/null @@ -1,592 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -## @package dbus_vrm -# This code takes care of the D-Bus interface (not all of below is implemented yet): -# - on startup it scans the dbus for services we know. For each known service found, it searches for -# objects/paths we know. Everything we find is stored in items{}, and an event is registered: if a -# value changes weĺl be notified and can pass that on to our owner. For example the vrmLogger. -# we know. -# - after startup, it continues to monitor the dbus: -# 1) when services are added we do the same check on that -# 2) when services are removed, we remove any items that we had that referred to that service -# 3) if an existing services adds paths we update ourselves as well: on init, we make a -# VeDbusItemImport for a non-, or not yet existing objectpaths as well1 -# -# Code is used by the vrmLogger, and also the pubsub code. Both are other modules in the dbus_vrm repo. - -from dbus.mainloop.glib import DBusGMainLoop -from gi.repository import GLib -import dbus -import dbus.service -import inspect -import logging -import argparse -import pprint -import traceback -import os -from collections import defaultdict -from functools import partial - -# our own packages -from ve_utils import exit_on_error, wrap_dbus_value, unwrap_dbus_value -notfound = object() # For lookups where None is a valid result - -logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) -class SystemBus(dbus.bus.BusConnection): - def __new__(cls): - return dbus.bus.BusConnection.__new__(cls, dbus.bus.BusConnection.TYPE_SYSTEM) - -class SessionBus(dbus.bus.BusConnection): - def __new__(cls): - return dbus.bus.BusConnection.__new__(cls, dbus.bus.BusConnection.TYPE_SESSION) - -class MonitoredValue(object): - def __init__(self, value, text, options): - super(MonitoredValue, self).__init__() - self.value = value - self.text = text - self.options = options - - # For legacy code, allow treating this as a tuple/list - def __iter__(self): - return iter((self.value, self.text, self.options)) - -class Service(object): - whentologoptions = ['configChange', 'onIntervalAlwaysAndOnEvent', - 'onIntervalOnlyWhenChanged', 'onIntervalAlways', 'never'] - def __init__(self, id, serviceName, deviceInstance): - super(Service, self).__init__() - self.id = id - self.name = serviceName - self.paths = {} - self._seen = set() - self.deviceInstance = deviceInstance - - self.configChange = [] - self.onIntervalAlwaysAndOnEvent = [] - self.onIntervalOnlyWhenChanged = [] - self.onIntervalAlways = [] - self.never = [] - - # For legacy code, attributes can still be accessed as if keys from a - # dictionary. - def __setitem__(self, key, value): - self.__dict__[key] = value - def __getitem__(self, key): - return self.__dict__[key] - - def set_seen(self, path): - self._seen.add(path) - - def seen(self, path): - return path in self._seen - - @property - def service_class(self): - return '.'.join(self.name.split('.')[:3]) - -class DbusMonitor(object): - ## Constructor - def __init__(self, dbusTree, valueChangedCallback=None, deviceAddedCallback=None, - deviceRemovedCallback=None, vebusDeviceInstance0=False): - # valueChangedCallback is the callback that we call when something has changed. - # def value_changed_on_dbus(dbusServiceName, dbusPath, options, changes, deviceInstance): - # in which changes is a tuple with GetText() and GetValue() - self.valueChangedCallback = valueChangedCallback - self.deviceAddedCallback = deviceAddedCallback - self.deviceRemovedCallback = deviceRemovedCallback - self.dbusTree = dbusTree - self.vebusDeviceInstance0 = vebusDeviceInstance0 - - # Lists all tracked services. Stores name, id, device instance, value per path, and whenToLog info - # indexed by service name (eg. com.victronenergy.settings). - self.servicesByName = {} - - # Same values as self.servicesByName, but indexed by service id (eg. :1.30) - self.servicesById = {} - - # Keep track of services by class to speed up calls to get_service_list - self.servicesByClass = defaultdict(list) - - # Keep track of any additional watches placed on items - self.serviceWatches = defaultdict(list) - - # For a PC, connect to the SessionBus - # For a CCGX, connect to the SystemBus - self.dbusConn = SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else SystemBus() - - # subscribe to NameOwnerChange for bus connect / disconnect events. - (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ \ - else dbus.SystemBus()).add_signal_receiver( - self.dbus_name_owner_changed, - signal_name='NameOwnerChanged') - - # Subscribe to PropertiesChanged for all services - self.dbusConn.add_signal_receiver(self.handler_value_changes, - dbus_interface='com.victronenergy.BusItem', - signal_name='PropertiesChanged', path_keyword='path', - sender_keyword='senderId') - - # Subscribe to ItemsChanged for all services - self.dbusConn.add_signal_receiver(self.handler_item_changes, - dbus_interface='com.victronenergy.BusItem', - signal_name='ItemsChanged', path='/', - sender_keyword='senderId') - - logger.info('===== Search on dbus for services that we will monitor starting... =====') - serviceNames = self.dbusConn.list_names() - for serviceName in serviceNames: - self.scan_dbus_service(serviceName) - - logger.info('===== Search on dbus for services that we will monitor finished =====') - - def dbus_name_owner_changed(self, name, oldowner, newowner): - if not name.startswith("com.victronenergy."): - return - - #decouple, and process in main loop - GLib.idle_add(exit_on_error, self._process_name_owner_changed, name, oldowner, newowner) - - def _process_name_owner_changed(self, name, oldowner, newowner): - if newowner != '': - # so we found some new service. Check if we can do something with it. - newdeviceadded = self.scan_dbus_service(name) - if newdeviceadded and self.deviceAddedCallback is not None: - self.deviceAddedCallback(name, self.get_device_instance(name)) - - elif name in self.servicesByName: - # it disappeared, we need to remove it. - logger.info("%s disappeared from the dbus. Removing it from our lists" % name) - service = self.servicesByName[name] - deviceInstance = service['deviceInstance'] - del self.servicesById[service.id] - del self.servicesByName[name] - for watch in self.serviceWatches[name]: - watch.remove() - del self.serviceWatches[name] - self.servicesByClass[service.service_class].remove(service) - if self.deviceRemovedCallback is not None: - self.deviceRemovedCallback(name, deviceInstance) - - def scan_dbus_service(self, serviceName): - try: - return self.scan_dbus_service_inner(serviceName) - except: - logger.error("Ignoring %s because of error while scanning:" % (serviceName)) - traceback.print_exc() - return False - - # Errors 'org.freedesktop.DBus.Error.ServiceUnknown' and - # 'org.freedesktop.DBus.Error.Disconnected' seem to happen when the service - # disappears while its being scanned. Which might happen, but is not really - # normal either, so letting them go into the logs. - - # Scans the given dbus service to see if it contains anything interesting for us. If it does, add - # it to our list of monitored D-Bus services. - def scan_dbus_service_inner(self, serviceName): - - # make it a normal string instead of dbus string - serviceName = str(serviceName) - - paths = self.dbusTree.get('.'.join(serviceName.split('.')[0:3]), None) - if paths is None: - logger.debug("Ignoring service %s, not in the tree" % serviceName) - return False - - logger.info("Found: %s, scanning and storing items" % serviceName) - serviceId = self.dbusConn.get_name_owner(serviceName) - - # we should never be notified to add a D-Bus service that we already have. If this assertion - # raises, check process_name_owner_changed, and D-Bus workings. - assert serviceName not in self.servicesByName - assert serviceId not in self.servicesById - - # for vebus.ttyO1, this is workaround, since VRM Portal expects the main vebus - # devices at instance 0. Not sure how to fix this yet. - if serviceName == 'com.victronenergy.vebus.ttyO1' and self.vebusDeviceInstance0: - di = 0 - elif serviceName == 'com.victronenergy.settings': - di = 0 - elif serviceName.startswith('com.victronenergy.vecan.'): - di = 0 - else: - try: - di = self.dbusConn.call_blocking(serviceName, - '/DeviceInstance', None, 'GetValue', '', []) - except dbus.exceptions.DBusException: - logger.info(" %s was skipped because it has no device instance" % serviceName) - return False # Skip it - else: - di = int(di) - - logger.info(" %s has device instance %s" % (serviceName, di)) - service = Service(serviceId, serviceName, di) - - # Let's try to fetch everything in one go - values = {} - texts = {} - try: - values.update(self.dbusConn.call_blocking(serviceName, '/', None, 'GetValue', '', [])) - texts.update(self.dbusConn.call_blocking(serviceName, '/', None, 'GetText', '', [])) - except: - pass - - for path, options in paths.items(): - # path will be the D-Bus path: '/Ac/ActiveIn/L1/V' - # options will be a dictionary: {'code': 'V', 'whenToLog': 'onIntervalAlways'} - # check that the whenToLog setting is set to something we expect - assert options['whenToLog'] is None or options['whenToLog'] in Service.whentologoptions - - # Try to obtain the value we want from our bulk fetch. If we - # cannot find it there, do an individual query. - value = values.get(path[1:], notfound) - if value != notfound: - service.set_seen(path) - text = texts.get(path[1:], notfound) - if value is notfound or text is notfound: - try: - value = self.dbusConn.call_blocking(serviceName, path, None, 'GetValue', '', []) - service.set_seen(path) - text = self.dbusConn.call_blocking(serviceName, path, None, 'GetText', '', []) - except dbus.exceptions.DBusException as e: - if e.get_dbus_name() in ( - 'org.freedesktop.DBus.Error.ServiceUnknown', - 'org.freedesktop.DBus.Error.Disconnected'): - raise # This exception will be handled below - - # TODO org.freedesktop.DBus.Error.UnknownMethod really - # shouldn't happen but sometimes does. - logger.debug("%s %s does not exist (yet)" % (serviceName, path)) - value = None - text = None - - service.paths[path] = MonitoredValue(unwrap_dbus_value(value), unwrap_dbus_value(text), options) - - if options['whenToLog']: - service[options['whenToLog']].append(path) - - - logger.debug("Finished scanning and storing items for %s" % serviceName) - - # Adjust self at the end of the scan, so we don't have an incomplete set of - # data if an exception occurs during the scan. - self.servicesByName[serviceName] = service - self.servicesById[serviceId] = service - self.servicesByClass[service.service_class].append(service) - - return True - - def handler_item_changes(self, items, senderId): - if not isinstance(items, dict): - return - - try: - service = self.servicesById[senderId] - except KeyError: - # senderId isn't there, which means it hasn't been scanned yet. - return - - for path, changes in items.items(): - try: - v = unwrap_dbus_value(changes['Value']) - except (KeyError, TypeError): - continue - - try: - t = changes['Text'] - except KeyError: - t = str(v) - self._handler_value_changes(service, path, v, t) - - def handler_value_changes(self, changes, path, senderId): - # If this properyChange does not involve a value, our work is done. - if 'Value' not in changes: - return - - try: - service = self.servicesById[senderId] - except KeyError: - # senderId isn't there, which means it hasn't been scanned yet. - return - - v = unwrap_dbus_value(changes['Value']) - # Some services don't send Text with their PropertiesChanged events. - try: - t = changes['Text'] - except KeyError: - t = str(v) - self._handler_value_changes(service, path, v, t) - - def _handler_value_changes(self, service, path, value, text): - try: - a = service.paths[path] - except KeyError: - # path isn't there, which means it hasn't been scanned yet. - return - - service.set_seen(path) - - # First update our store to the new value - if a.value == value: - return - - a.value = value - a.text = text - - # And do the rest of the processing in on the mainloop - if self.valueChangedCallback is not None: - GLib.idle_add(exit_on_error, self._execute_value_changes, service.name, path, { - 'Value': value, 'Text': text}, a.options) - - def _execute_value_changes(self, serviceName, objectPath, changes, options): - # double check that the service still exists, as it might have - # disappeared between scheduling-for and executing this function. - if serviceName not in self.servicesByName: - return - - self.valueChangedCallback(serviceName, objectPath, - options, changes, self.get_device_instance(serviceName)) - - # Gets the value for a certain servicename and path - # The default_value is returned when: - # 1. When the service doesn't exist. - # 2. When the path asked for isn't being monitored. - # 3. When the path exists, but has dbus-invalid, ie an empty byte array. - # 4. When the path asked for is being monitored, but doesn't exist for that service. - def get_value(self, serviceName, objectPath, default_value=None): - service = self.servicesByName.get(serviceName, None) - if service is None: - return default_value - - value = service.paths.get(objectPath, None) - if value is None or value.value is None: - return default_value - - return value.value - - # returns if a dbus exists now, by doing a blocking dbus call. - # Typically seen will be sufficient and doesn't need access to the dbus. - def exists(self, serviceName, objectPath): - try: - self.dbusConn.call_blocking(serviceName, objectPath, None, 'GetValue', '', []) - return True - except dbus.exceptions.DBusException as e: - return False - - # Returns if there ever was a successful GetValue or valueChanged event. - # Unlike get_value this return True also if the actual value is invalid. - # - # Note: the path might no longer exists anymore, but that doesn't happen in - # practice. If a service really wants to reconfigure itself typically it should - # reconnect to the dbus which causes it to be rescanned and seen will be updated. - # If it is really needed to know if a path still exists, use exists. - def seen(self, serviceName, objectPath): - try: - return self.servicesByName[serviceName].seen(objectPath) - except KeyError: - return False - - # Sets the value for a certain servicename and path, returns the return value of the D-Bus SetValue - # method. If the underlying item does not exist (the service does not exist, or the objectPath was not - # registered) the function will return -1 - def set_value(self, serviceName, objectPath, value): - # Check if the D-Bus object referenced by serviceName and objectPath is registered. There is no - # necessity to do this, but it is in line with previous implementations which kept VeDbusItemImport - # objects for registers items only. - service = self.servicesByName.get(serviceName, None) - if service is None: - return -1 - if objectPath not in service.paths: - return -1 - # We do not catch D-Bus exceptions here, because the previous implementation did not do that either. - return self.dbusConn.call_blocking(serviceName, objectPath, - dbus_interface='com.victronenergy.BusItem', - method='SetValue', signature=None, - args=[wrap_dbus_value(value)]) - - # Similar to set_value, but operates asynchronously - def set_value_async(self, serviceName, objectPath, value, - reply_handler=None, error_handler=None): - service = self.servicesByName.get(serviceName, None) - if service is not None: - if objectPath in service.paths: - self.dbusConn.call_async(serviceName, objectPath, - dbus_interface='com.victronenergy.BusItem', - method='SetValue', signature=None, - args=[wrap_dbus_value(value)], - reply_handler=reply_handler, error_handler=error_handler) - return - - if error_handler is not None: - error_handler(TypeError('Service or path not found, ' - 'service=%s, path=%s' % (serviceName, objectPath))) - - # returns a dictionary, keys are the servicenames, value the instances - # optionally use the classfilter to get only a certain type of services, for - # example com.victronenergy.battery. - def get_service_list(self, classfilter=None): - if classfilter is None: - return { servicename: service.deviceInstance \ - for servicename, service in self.servicesByName.items() } - - if classfilter not in self.servicesByClass: - return {} - - return { service.name: service.deviceInstance \ - for service in self.servicesByClass[classfilter] } - - def get_device_instance(self, serviceName): - return self.servicesByName[serviceName].deviceInstance - - # Parameter categoryfilter is to be a list, containing the categories you want (configChange, - # onIntervalAlways, etc). - # Returns a dictionary, keys are codes + instance, in VRM querystring format. For example vvt[0]. And - # values are the value. - def get_values(self, categoryfilter, converter=None): - - result = {} - - for serviceName in self.servicesByName: - result.update(self.get_values_for_service(categoryfilter, serviceName, converter)) - - return result - - # same as get_values above, but then for one service only - def get_values_for_service(self, categoryfilter, servicename, converter=None): - deviceInstance = self.get_device_instance(servicename) - result = {} - - service = self.servicesByName[servicename] - - for category in categoryfilter: - - for path in service[category]: - - value, text, options = service.paths[path] - - if value is not None: - - value = value if converter is None else converter.convert(path, options['code'], value, text) - - precision = options.get('precision') - if precision: - value = round(value, precision) - - result[options['code'] + "[" + str(deviceInstance) + "]"] = value - - return result - - def track_value(self, serviceName, objectPath, callback, *args, **kwargs): - """ A DbusMonitor can watch specific service/path combos for changes - so that it is not fully reliant on the global handler_value_changes - in this class. Additional watches are deleted automatically when - the service disappears from dbus. """ - cb = partial(callback, *args, **kwargs) - - def root_tracker(items): - # Check if objectPath in dict - try: - v = items[objectPath] - _v = unwrap_dbus_value(v['Value']) - except (KeyError, TypeError): - return # not in this dict - - try: - t = v['Text'] - except KeyError: - cb({'Value': _v }) - else: - cb({'Value': _v, 'Text': t}) - - # Track changes on the path, and also on root - self.serviceWatches[serviceName].extend(( - self.dbusConn.add_signal_receiver(cb, - dbus_interface='com.victronenergy.BusItem', - signal_name='PropertiesChanged', - path=objectPath, bus_name=serviceName), - self.dbusConn.add_signal_receiver(root_tracker, - dbus_interface='com.victronenergy.BusItem', - signal_name='ItemsChanged', - path="/", bus_name=serviceName), - )) - - -# ====== ALL CODE BELOW THIS LINE IS PURELY FOR DEVELOPING THIS CLASS ====== - -# Example function that can be used as a starting point to use this code -def value_changed_on_dbus(dbusServiceName, dbusPath, dict, changes, deviceInstance): - logger.debug("0 ----------------") - logger.debug("1 %s%s changed" % (dbusServiceName, dbusPath)) - logger.debug("2 vrm dict : %s" % dict) - logger.debug("3 changes-text: %s" % changes['Text']) - logger.debug("4 changes-value: %s" % changes['Value']) - logger.debug("5 deviceInstance: %s" % deviceInstance) - logger.debug("6 - end") - - -def nameownerchange(a, b): - # used to find memory leaks in dbusmonitor and VeDbusItemImport - import gc - gc.collect() - objects = gc.get_objects() - print (len([o for o in objects if type(o).__name__ == 'VeDbusItemImport'])) - print (len([o for o in objects if type(o).__name__ == 'SignalMatch'])) - print (len(objects)) - - -def print_values(dbusmonitor): - a = dbusmonitor.get_value('wrongservice', '/DbusInvalid', default_value=1000) - b = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/NotInTheMonitorList', default_value=1000) - c = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/DbusInvalid', default_value=1000) - d = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/NonExistingButMonitored', default_value=1000) - - print ("All should be 1000: Wrong Service: %s, NotInTheMonitorList: %s, DbusInvalid: %s, NonExistingButMonitored: %s" % (a, b, c, d)) - return True - -# We have a mainloop, but that is just for developing this code. Normally above class & code is used from -# some other class, such as vrmLogger or the pubsub Implementation. -def main(): - # Init logging - logging.basicConfig(level=logging.DEBUG) - logger.info(__file__ + " is starting up") - - # Have a mainloop, so we can send/receive asynchronous calls to and from dbus - DBusGMainLoop(set_as_default=True) - - import os - import sys - sys.path.insert(1, os.path.join(os.path.dirname(__file__), '../../')) - - dummy = {'code': None, 'whenToLog': 'configChange', 'accessLevel': None} - monitorlist = {'com.victronenergy.dummyservice': { - '/Connected': dummy, - '/ProductName': dummy, - '/Mgmt/Connection': dummy, - '/Dc/0/Voltage': dummy, - '/Dc/0/Current': dummy, - '/Dc/0/Temperature': dummy, - '/Load/I': dummy, - '/FirmwareVersion': dummy, - '/DbusInvalid': dummy, - '/NonExistingButMonitored': dummy}} - - d = DbusMonitor(monitorlist, value_changed_on_dbus, - deviceAddedCallback=nameownerchange, deviceRemovedCallback=nameownerchange) - - # logger.info("==configchange values==") - # logger.info(pprint.pformat(d.get_values(['configChange']))) - - # logger.info("==onIntervalAlways and onIntervalOnlyWhenChanged==") - # logger.info(pprint.pformat(d.get_values(['onIntervalAlways', 'onIntervalAlwaysAndOnEvent']))) - - GLib.timeout_add(1000, print_values, d) - - # Start and run the mainloop - logger.info("Starting mainloop, responding on only events") - mainloop = GLib.MainLoop() - mainloop.run() - -if __name__ == "__main__": - main() diff --git a/velib_python/velib_python/v2.94/oldestVersion b/velib_python/velib_python/v2.94/oldestVersion deleted file mode 100644 index 366edb3..0000000 --- a/velib_python/velib_python/v2.94/oldestVersion +++ /dev/null @@ -1 +0,0 @@ -v2.80 diff --git a/velib_python/velib_python/v2.94/settingsdevice.py b/velib_python/velib_python/v2.94/settingsdevice.py deleted file mode 100644 index a207e8b..0000000 --- a/velib_python/velib_python/v2.94/settingsdevice.py +++ /dev/null @@ -1,118 +0,0 @@ -import dbus -import logging -import time -from functools import partial - -# Local imports -from vedbus import VeDbusItemImport - -## Indexes for the setting dictonary. -PATH = 0 -VALUE = 1 -MINIMUM = 2 -MAXIMUM = 3 -SILENT = 4 - -## The Settings Device class. -# Used by python programs, such as the vrm-logger, to read and write settings they -# need to store on disk. And since these settings might be changed from a different -# source, such as the GUI, the program can pass an eventCallback that will be called -# as soon as some setting is changed. -# -# The settings are stored in flash via the com.victronenergy.settings service on dbus. -# See https://github.com/victronenergy/localsettings for more info. -# -# If there are settings in de supportSettings list which are not yet on the dbus, -# and therefore not yet in the xml file, they will be added through the dbus-addSetting -# interface of com.victronenergy.settings. -class SettingsDevice(object): - ## The constructor processes the tree of dbus-items. - # @param bus the system-dbus object - # @param name the dbus-service-name of the settings dbus service, 'com.victronenergy.settings' - # @param supportedSettings dictionary with all setting-names, and their defaultvalue, min, max and whether - # the setting is silent. The 'silent' entry is optional. If set to true, no changes in the setting will - # be logged by localsettings. - # @param eventCallback function that will be called on changes on any of these settings - # @param timeout Maximum interval to wait for localsettings. An exception is thrown at the end of the - # interval if the localsettings D-Bus service has not appeared yet. - def __init__(self, bus, supportedSettings, eventCallback, name='com.victronenergy.settings', timeout=0): - logging.debug("===== Settings device init starting... =====") - self._bus = bus - self._dbus_name = name - self._eventCallback = eventCallback - self._values = {} # stored the values, used to pass the old value along on a setting change - self._settings = {} - - count = 0 - while True: - if 'com.victronenergy.settings' in self._bus.list_names(): - break - if count == timeout: - raise Exception("The settings service com.victronenergy.settings does not exist!") - count += 1 - logging.info('waiting for settings') - time.sleep(1) - - # Add the items. - self.addSettings(supportedSettings) - - logging.debug("===== Settings device init finished =====") - - def addSettings(self, settings): - for setting, options in settings.items(): - silent = len(options) > SILENT and options[SILENT] - busitem = self.addSetting(options[PATH], options[VALUE], - options[MINIMUM], options[MAXIMUM], silent, callback=partial(self.handleChangedSetting, setting)) - self._settings[setting] = busitem - self._values[setting] = busitem.get_value() - - def addSetting(self, path, value, _min, _max, silent=False, callback=None): - busitem = VeDbusItemImport(self._bus, self._dbus_name, path, callback) - if busitem.exists and (value, _min, _max, silent) == busitem._proxy.GetAttributes(): - logging.debug("Setting %s found" % path) - else: - logging.info("Setting %s does not exist yet or must be adjusted" % path) - - # Prepare to add the setting. Most dbus types extend the python - # type so it is only necessary to additionally test for Int64. - if isinstance(value, (int, dbus.Int64)): - itemType = 'i' - elif isinstance(value, float): - itemType = 'f' - else: - itemType = 's' - - # Add the setting - # TODO, make an object that inherits VeDbusItemImport, and complete the D-Bus settingsitem interface - settings_item = VeDbusItemImport(self._bus, self._dbus_name, '/Settings', createsignal=False) - setting_path = path.replace('/Settings/', '', 1) - if silent: - settings_item._proxy.AddSilentSetting('', setting_path, value, itemType, _min, _max) - else: - settings_item._proxy.AddSetting('', setting_path, value, itemType, _min, _max) - - busitem = VeDbusItemImport(self._bus, self._dbus_name, path, callback) - - return busitem - - def handleChangedSetting(self, setting, servicename, path, changes): - oldvalue = self._values[setting] if setting in self._values else None - self._values[setting] = changes['Value'] - - if self._eventCallback is None: - return - - self._eventCallback(setting, oldvalue, changes['Value']) - - def setDefault(self, path): - item = VeDbusItemImport(self._bus, self._dbus_name, path, createsignal=False) - item.set_default() - - def __getitem__(self, setting): - return self._settings[setting].get_value() - - def __setitem__(self, setting, newvalue): - result = self._settings[setting].set_value(newvalue) - if result != 0: - # Trying to make some false change to our own settings? How dumb! - assert False diff --git a/velib_python/velib_python/v2.94/ve_utils.py b/velib_python/velib_python/v2.94/ve_utils.py deleted file mode 100644 index e8d847d..0000000 --- a/velib_python/velib_python/v2.94/ve_utils.py +++ /dev/null @@ -1,265 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -import sys -from traceback import print_exc -from os import _exit as os_exit -from os import statvfs -from subprocess import check_output, CalledProcessError -import logging -import dbus -logger = logging.getLogger(__name__) - -VEDBUS_INVALID = dbus.Array([], signature=dbus.Signature('i'), variant_level=1) - -class NoVrmPortalIdError(Exception): - pass - -# Use this function to make sure the code quits on an unexpected exception. Make sure to use it -# when using GLib.idle_add and also GLib.timeout_add. -# Without this, the code will just keep running, since GLib does not stop the mainloop on an -# exception. -# Example: GLib.idle_add(exit_on_error, myfunc, arg1, arg2) -def exit_on_error(func, *args, **kwargs): - try: - return func(*args, **kwargs) - except: - try: - print ('exit_on_error: there was an exception. Printing stacktrace will be tried and then exit') - print_exc() - except: - pass - - # sys.exit() is not used, since that throws an exception, which does not lead to a program - # halt when used in a dbus callback, see connection.py in the Python/Dbus libraries, line 230. - os_exit(1) - - -__vrm_portal_id = None -def get_vrm_portal_id(): - # The original definition of the VRM Portal ID is that it is the mac - # address of the onboard- ethernet port (eth0), stripped from its colons - # (:) and lower case. This may however differ between platforms. On Venus - # the task is therefore deferred to /sbin/get-unique-id so that a - # platform specific method can be easily defined. - # - # If /sbin/get-unique-id does not exist, then use the ethernet address - # of eth0. This also handles the case where velib_python is used as a - # package install on a Raspberry Pi. - # - # On a Linux host where the network interface may not be eth0, you can set - # the VRM_IFACE environment variable to the correct name. - - global __vrm_portal_id - - if __vrm_portal_id: - return __vrm_portal_id - - portal_id = None - - # First try the method that works if we don't have a data partition. This - # will fail when the current user is not root. - try: - portal_id = check_output("/sbin/get-unique-id").decode("utf-8", "ignore").strip() - if not portal_id: - raise NoVrmPortalIdError("get-unique-id returned blank") - __vrm_portal_id = portal_id - return portal_id - except CalledProcessError: - # get-unique-id returned non-zero - raise NoVrmPortalIdError("get-unique-id returned non-zero") - except OSError: - # File doesn't exist, use fallback - pass - - # Fall back to getting our id using a syscall. Assume we are on linux. - # Allow the user to override what interface is used using an environment - # variable. - import fcntl, socket, struct, os - - iface = os.environ.get('VRM_IFACE', 'eth0').encode('ascii') - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - try: - info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', iface[:15])) - except IOError: - raise NoVrmPortalIdError("ioctl failed for eth0") - - __vrm_portal_id = info[18:24].hex() - return __vrm_portal_id - - -# See VE.Can registers - public.docx for definition of this conversion -def convert_vreg_version_to_readable(version): - def str_to_arr(x, length): - a = [] - for i in range(0, len(x), length): - a.append(x[i:i+length]) - return a - - x = "%x" % version - x = x.upper() - - if len(x) == 5 or len(x) == 3 or len(x) == 1: - x = '0' + x - - a = str_to_arr(x, 2); - - # remove the first 00 if there are three bytes and it is 00 - if len(a) == 3 and a[0] == '00': - a.remove(0); - - # if we have two or three bytes now, and the first character is a 0, remove it - if len(a) >= 2 and a[0][0:1] == '0': - a[0] = a[0][1]; - - result = '' - for item in a: - result += ('.' if result != '' else '') + item - - - result = 'v' + result - - return result - - -def get_free_space(path): - result = -1 - - try: - s = statvfs(path) - result = s.f_frsize * s.f_bavail # Number of free bytes that ordinary users - except Exception as ex: - logger.info("Error while retrieving free space for path %s: %s" % (path, ex)) - - return result - - -def get_load_averages(): - c = read_file('/proc/loadavg') - return c.split(' ')[:3] - - -def _get_sysfs_machine_name(): - try: - with open('/sys/firmware/devicetree/base/model', 'r') as f: - return f.read().rstrip('\x00') - except IOError: - pass - - return None - -# Returns None if it cannot find a machine name. Otherwise returns the string -# containing the name -def get_machine_name(): - # First try calling the venus utility script - try: - return check_output("/usr/bin/product-name").strip().decode('UTF-8') - except (CalledProcessError, OSError): - pass - - # Fall back to sysfs - name = _get_sysfs_machine_name() - if name is not None: - return name - - # Fall back to venus build machine name - try: - with open('/etc/venus/machine', 'r', encoding='UTF-8') as f: - return f.read().strip() - except IOError: - pass - - return None - - -def get_product_id(): - """ Find the machine ID and return it. """ - - # First try calling the venus utility script - try: - return check_output("/usr/bin/product-id").strip() - except (CalledProcessError, OSError): - pass - - # Fall back machine name mechanism - name = _get_sysfs_machine_name() - return { - 'Color Control GX': 'C001', - 'Venus GX': 'C002', - 'Octo GX': 'C006', - 'EasySolar-II': 'C007', - 'MultiPlus-II': 'C008' - }.get(name, 'C003') # C003 is Generic - - -# Returns False if it cannot open the file. Otherwise returns its rstripped contents -def read_file(path): - content = False - - try: - with open(path, 'r') as f: - content = f.read().rstrip() - except Exception as ex: - logger.debug("Error while reading %s: %s" % (path, ex)) - - return content - - -def wrap_dbus_value(value): - if value is None: - return VEDBUS_INVALID - if isinstance(value, float): - return dbus.Double(value, variant_level=1) - if isinstance(value, bool): - return dbus.Boolean(value, variant_level=1) - if isinstance(value, int): - try: - return dbus.Int32(value, variant_level=1) - except OverflowError: - return dbus.Int64(value, variant_level=1) - if isinstance(value, str): - return dbus.String(value, variant_level=1) - if isinstance(value, list): - if len(value) == 0: - # If the list is empty we cannot infer the type of the contents. So assume unsigned integer. - # A (signed) integer is dangerous, because an empty list of signed integers is used to encode - # an invalid value. - return dbus.Array([], signature=dbus.Signature('u'), variant_level=1) - return dbus.Array([wrap_dbus_value(x) for x in value], variant_level=1) - if isinstance(value, dict): - # Wrapping the keys of the dictionary causes D-Bus errors like: - # 'arguments to dbus_message_iter_open_container() were incorrect, - # assertion "(type == DBUS_TYPE_ARRAY && contained_signature && - # *contained_signature == DBUS_DICT_ENTRY_BEGIN_CHAR) || (contained_signature == NULL || - # _dbus_check_is_valid_signature (contained_signature))" failed in file ...' - return dbus.Dictionary({(k, wrap_dbus_value(v)) for k, v in value.items()}, variant_level=1) - return value - - -dbus_int_types = (dbus.Int32, dbus.UInt32, dbus.Byte, dbus.Int16, dbus.UInt16, dbus.UInt32, dbus.Int64, dbus.UInt64) - - -def unwrap_dbus_value(val): - """Converts D-Bus values back to the original type. For example if val is of type DBus.Double, - a float will be returned.""" - if isinstance(val, dbus_int_types): - return int(val) - if isinstance(val, dbus.Double): - return float(val) - if isinstance(val, dbus.Array): - v = [unwrap_dbus_value(x) for x in val] - return None if len(v) == 0 else v - if isinstance(val, (dbus.Signature, dbus.String)): - return str(val) - # Python has no byte type, so we convert to an integer. - if isinstance(val, dbus.Byte): - return int(val) - if isinstance(val, dbus.ByteArray): - return "".join([bytes(x) for x in val]) - if isinstance(val, (list, tuple)): - return [unwrap_dbus_value(x) for x in val] - if isinstance(val, (dbus.Dictionary, dict)): - # Do not unwrap the keys, see comment in wrap_dbus_value - return dict([(x, unwrap_dbus_value(y)) for x, y in val.items()]) - if isinstance(val, dbus.Boolean): - return bool(val) - return val diff --git a/velib_python/velib_python/v2.94/vedbus.py b/velib_python/velib_python/v2.94/vedbus.py deleted file mode 100644 index d6dac60..0000000 --- a/velib_python/velib_python/v2.94/vedbus.py +++ /dev/null @@ -1,599 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import dbus.service -import logging -import traceback -import os -import weakref -from collections import defaultdict -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 = {} - self._ratelimiters = [] - - # 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['/'] = VeDbusRootExport(self._dbusconn, '/', self) - - logging.info("registered ourselves on D-Bus as %s" % servicename) - - # To force immediate deregistering of this dbus service and all its object paths, explicitly - # call __del__(). - def __del__(self): - for node in list(self._dbusnodes.values()): - node.__del__() - self._dbusnodes.clear() - for item in list(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] = VeDbusTreeExport(self._dbusconn, subPath, self) - 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) - - # 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 list(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 - - def __enter__(self): - l = ServiceContext(self) - self._ratelimiters.append(l) - return l - - def __exit__(self, *exc): - # pop off the top one and flush it. If with statements are nested - # then each exit flushes its own part. - if self._ratelimiters: - self._ratelimiters.pop().flush() - -class ServiceContext(object): - def __init__(self, parent): - self.parent = parent - self.changes = {} - - def __getitem__(self, path): - return self.parent[path] - - def __setitem__(self, path, newvalue): - c = self.parent._dbusobjects[path]._local_set_value(newvalue) - if c is not None: - self.changes[path] = c - - def flush(self): - if self.changes: - self.parent._dbusnodes['/'].ItemsChanged(self.changes) - -class TrackerDict(defaultdict): - """ Same as defaultdict, but passes the key to default_factory. """ - def __missing__(self, key): - self[key] = x = self.default_factory(key) - return x - -class VeDbusRootTracker(object): - """ This tracks the root of a dbus path and listens for PropertiesChanged - signals. When a signal arrives, parse it and unpack the key/value changes - into traditional events, then pass it to the original eventCallback - method. """ - def __init__(self, bus, serviceName): - self.importers = defaultdict(weakref.WeakSet) - self.serviceName = serviceName - self._match = bus.get_object(serviceName, '/', introspect=False).connect_to_signal( - "ItemsChanged", weak_functor(self._items_changed_handler)) - - def __del__(self): - self._match.remove() - self._match = None - - def add(self, i): - self.importers[i.path].add(i) - - def _items_changed_handler(self, items): - if not isinstance(items, dict): - return - - for path, changes in items.items(): - try: - v = changes['Value'] - except KeyError: - continue - - try: - t = changes['Text'] - except KeyError: - t = str(unwrap_dbus_value(v)) - - for i in self.importers.get(path, ()): - i._properties_changed_handler({'Value': v, 'Text': t}) - -""" -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): - def __new__(cls, bus, serviceName, path, eventCallback=None, createsignal=True): - instance = object.__new__(cls) - - # If signal tracking should be done, also add to root tracker - if createsignal: - if "_roots" not in cls.__dict__: - cls._roots = TrackerDict(lambda k: VeDbusRootTracker(bus, k)) - - return instance - - ## 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)) - self._roots[serviceName].add(self) - - # 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 is not 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 - - ## Resets the item to its default value - def set_default(self): - self._proxy.SetDefault() - self._refreshcachedvalue() - - ## 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, service): - dbus.service.Object.__init__(self, bus, objectPath) - self._service = service - 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] - - def _get_value_handler(self, path, get_text=False): - logging.debug("_get_value_handler called for %s" % path) - r = {} - px = path - if not px.endswith('/'): - px += '/' - for p, item in self._service._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 - - @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 VeDbusRootExport(VeDbusTreeExport): - @dbus.service.signal('com.victronenergy.BusItem', signature='a{sa{sv}}') - def ItemsChanged(self, changes): - pass - - @dbus.service.method('com.victronenergy.BusItem', out_signature='a{sa{sv}}') - def GetItems(self): - return { - path: { - 'Value': wrap_dbus_value(item.local_get_value()), - 'Text': item.GetText() } - for path, item in self._service._dbusobjects.items() - } - - -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): - changes = self._local_set_value(newvalue) - if changes is not None: - self.PropertiesChanged(changes) - - def _local_set_value(self, newvalue): - if self._value == newvalue: - return None - - self._value = newvalue - return { - 'Value': wrap_dbus_value(newvalue), - 'Text': self.GetText() - } - - 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) diff --git a/velib_python/velib_python/v3.40~37/dbusmonitor.py b/velib_python/velib_python/v3.40~37/dbusmonitor.py deleted file mode 100644 index cb2185d..0000000 --- a/velib_python/velib_python/v3.40~37/dbusmonitor.py +++ /dev/null @@ -1,554 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -## @package dbus_vrm -# This code takes care of the D-Bus interface (not all of below is implemented yet): -# - on startup it scans the dbus for services we know. For each known service found, it searches for -# objects/paths we know. Everything we find is stored in items{}, and an event is registered: if a -# value changes weĺl be notified and can pass that on to our owner. For example the vrmLogger. -# we know. -# - after startup, it continues to monitor the dbus: -# 1) when services are added we do the same check on that -# 2) when services are removed, we remove any items that we had that referred to that service -# 3) if an existing services adds paths we update ourselves as well: on init, we make a -# VeDbusItemImport for a non-, or not yet existing objectpaths as well1 -# -# Code is used by the vrmLogger, and also the pubsub code. Both are other modules in the dbus_vrm repo. - -from dbus.mainloop.glib import DBusGMainLoop -from gi.repository import GLib -import dbus -import dbus.service -import inspect -import logging -import argparse -import pprint -import traceback -import os -from collections import defaultdict -from functools import partial - -# our own packages -from ve_utils import exit_on_error, wrap_dbus_value, unwrap_dbus_value -notfound = object() # For lookups where None is a valid result - -logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) -class SystemBus(dbus.bus.BusConnection): - def __new__(cls): - return dbus.bus.BusConnection.__new__(cls, dbus.bus.BusConnection.TYPE_SYSTEM) - -class SessionBus(dbus.bus.BusConnection): - def __new__(cls): - return dbus.bus.BusConnection.__new__(cls, dbus.bus.BusConnection.TYPE_SESSION) - -class MonitoredValue(object): - def __init__(self, value, text, options): - super(MonitoredValue, self).__init__() - self.value = value - self.text = text - self.options = options - - # For legacy code, allow treating this as a tuple/list - def __iter__(self): - return iter((self.value, self.text, self.options)) - -class Service(object): - def __init__(self, id, serviceName, deviceInstance): - super(Service, self).__init__() - self.id = id - self.name = serviceName - self.paths = {} - self._seen = set() - self.deviceInstance = deviceInstance - - # For legacy code, attributes can still be accessed as if keys from a - # dictionary. - def __setitem__(self, key, value): - self.__dict__[key] = value - def __getitem__(self, key): - return self.__dict__[key] - - def set_seen(self, path): - self._seen.add(path) - - def seen(self, path): - return path in self._seen - - @property - def service_class(self): - return '.'.join(self.name.split('.')[:3]) - -class DbusMonitor(object): - ## Constructor - def __init__(self, dbusTree, valueChangedCallback=None, deviceAddedCallback=None, - deviceRemovedCallback=None, namespace="com.victronenergy"): - # valueChangedCallback is the callback that we call when something has changed. - # def value_changed_on_dbus(dbusServiceName, dbusPath, options, changes, deviceInstance): - # in which changes is a tuple with GetText() and GetValue() - self.valueChangedCallback = valueChangedCallback - self.deviceAddedCallback = deviceAddedCallback - self.deviceRemovedCallback = deviceRemovedCallback - self.dbusTree = dbusTree - - # Lists all tracked services. Stores name, id, device instance, value per path, and whenToLog info - # indexed by service name (eg. com.victronenergy.settings). - self.servicesByName = {} - - # Same values as self.servicesByName, but indexed by service id (eg. :1.30) - self.servicesById = {} - - # Keep track of services by class to speed up calls to get_service_list - self.servicesByClass = defaultdict(list) - - # Keep track of any additional watches placed on items - self.serviceWatches = defaultdict(list) - - # For a PC, connect to the SessionBus - # For a CCGX, connect to the SystemBus - self.dbusConn = SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else SystemBus() - - # subscribe to NameOwnerChange for bus connect / disconnect events. - # NOTE: this is on a different bus then the one above! - standardBus = (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ \ - else dbus.SystemBus()) - - self.add_name_owner_changed_receiver(standardBus, self.dbus_name_owner_changed) - - # Subscribe to PropertiesChanged for all services - self.dbusConn.add_signal_receiver(self.handler_value_changes, - dbus_interface='com.victronenergy.BusItem', - signal_name='PropertiesChanged', path_keyword='path', - sender_keyword='senderId') - - # Subscribe to ItemsChanged for all services - self.dbusConn.add_signal_receiver(self.handler_item_changes, - dbus_interface='com.victronenergy.BusItem', - signal_name='ItemsChanged', path='/', - sender_keyword='senderId') - - logger.info('===== Search on dbus for services that we will monitor starting... =====') - serviceNames = self.dbusConn.list_names() - for serviceName in serviceNames: - self.scan_dbus_service(serviceName) - - logger.info('===== Search on dbus for services that we will monitor finished =====') - - @staticmethod - def make_service(serviceId, serviceName, deviceInstance): - """ Override this to use a different kind of service object. """ - return Service(serviceId, serviceName, deviceInstance) - - def make_monitor(self, service, path, value, text, options): - """ Override this to do more things with monitoring. """ - return MonitoredValue(unwrap_dbus_value(value), unwrap_dbus_value(text), options) - - def dbus_name_owner_changed(self, name, oldowner, newowner): - if not name.startswith("com.victronenergy."): - return - - #decouple, and process in main loop - GLib.idle_add(exit_on_error, self._process_name_owner_changed, name, oldowner, newowner) - - @staticmethod - # When supported, only name owner changes for the the given namespace are reported. This - # prevents spending cpu time at irrelevant changes, like scripts accessing the bus temporarily. - def add_name_owner_changed_receiver(dbus, name_owner_changed, namespace="com.victronenergy"): - # support for arg0namespace is submitted upstream, but not included at the time of - # writing, Venus OS does support it, so try if it works. - if namespace is None: - dbus.add_signal_receiver(name_owner_changed, signal_name='NameOwnerChanged') - else: - try: - dbus.add_signal_receiver(name_owner_changed, - signal_name='NameOwnerChanged', arg0namespace=namespace) - except TypeError: - dbus.add_signal_receiver(name_owner_changed, signal_name='NameOwnerChanged') - - def _process_name_owner_changed(self, name, oldowner, newowner): - if newowner != '': - # so we found some new service. Check if we can do something with it. - newdeviceadded = self.scan_dbus_service(name) - if newdeviceadded and self.deviceAddedCallback is not None: - self.deviceAddedCallback(name, self.get_device_instance(name)) - - elif name in self.servicesByName: - # it disappeared, we need to remove it. - logger.info("%s disappeared from the dbus. Removing it from our lists" % name) - service = self.servicesByName[name] - del self.servicesById[service.id] - del self.servicesByName[name] - for watch in self.serviceWatches[name]: - watch.remove() - del self.serviceWatches[name] - self.servicesByClass[service.service_class].remove(service) - if self.deviceRemovedCallback is not None: - self.deviceRemovedCallback(name, service.deviceInstance) - - def scan_dbus_service(self, serviceName): - try: - return self.scan_dbus_service_inner(serviceName) - except: - logger.error("Ignoring %s because of error while scanning:" % (serviceName)) - traceback.print_exc() - return False - - # Errors 'org.freedesktop.DBus.Error.ServiceUnknown' and - # 'org.freedesktop.DBus.Error.Disconnected' seem to happen when the service - # disappears while its being scanned. Which might happen, but is not really - # normal either, so letting them go into the logs. - - # Scans the given dbus service to see if it contains anything interesting for us. If it does, add - # it to our list of monitored D-Bus services. - def scan_dbus_service_inner(self, serviceName): - - # make it a normal string instead of dbus string - serviceName = str(serviceName) - - paths = self.dbusTree.get('.'.join(serviceName.split('.')[0:3]), None) - if paths is None: - logger.debug("Ignoring service %s, not in the tree" % serviceName) - return False - - logger.info("Found: %s, scanning and storing items" % serviceName) - serviceId = self.dbusConn.get_name_owner(serviceName) - - # we should never be notified to add a D-Bus service that we already have. If this assertion - # raises, check process_name_owner_changed, and D-Bus workings. - assert serviceName not in self.servicesByName - assert serviceId not in self.servicesById - - if serviceName == 'com.victronenergy.settings': - di = 0 - elif serviceName.startswith('com.victronenergy.vecan.'): - di = 0 - else: - try: - di = self.dbusConn.call_blocking(serviceName, - '/DeviceInstance', None, 'GetValue', '', []) - except dbus.exceptions.DBusException: - logger.info(" %s was skipped because it has no device instance" % serviceName) - return False # Skip it - else: - di = int(di) - - logger.info(" %s has device instance %s" % (serviceName, di)) - service = self.make_service(serviceId, serviceName, di) - - # Let's try to fetch everything in one go - values = {} - texts = {} - try: - values.update(self.dbusConn.call_blocking(serviceName, '/', None, 'GetValue', '', [])) - texts.update(self.dbusConn.call_blocking(serviceName, '/', None, 'GetText', '', [])) - except: - pass - - for path, options in paths.items(): - # path will be the D-Bus path: '/Ac/ActiveIn/L1/V' - # options will be a dictionary: {'code': 'V', 'whenToLog': 'onIntervalAlways'} - - # Try to obtain the value we want from our bulk fetch. If we - # cannot find it there, do an individual query. - value = values.get(path[1:], notfound) - if value != notfound: - service.set_seen(path) - text = texts.get(path[1:], notfound) - if value is notfound or text is notfound: - try: - value = self.dbusConn.call_blocking(serviceName, path, None, 'GetValue', '', []) - service.set_seen(path) - text = self.dbusConn.call_blocking(serviceName, path, None, 'GetText', '', []) - except dbus.exceptions.DBusException as e: - if e.get_dbus_name() in ( - 'org.freedesktop.DBus.Error.ServiceUnknown', - 'org.freedesktop.DBus.Error.Disconnected'): - raise # This exception will be handled below - - # TODO org.freedesktop.DBus.Error.UnknownMethod really - # shouldn't happen but sometimes does. - logger.debug("%s %s does not exist (yet)" % (serviceName, path)) - value = None - text = None - - service.paths[path] = self.make_monitor(service, path, unwrap_dbus_value(value), unwrap_dbus_value(text), options) - - - logger.debug("Finished scanning and storing items for %s" % serviceName) - - # Adjust self at the end of the scan, so we don't have an incomplete set of - # data if an exception occurs during the scan. - self.servicesByName[serviceName] = service - self.servicesById[serviceId] = service - self.servicesByClass[service.service_class].append(service) - - return True - - def handler_item_changes(self, items, senderId): - if not isinstance(items, dict): - return - - try: - service = self.servicesById[senderId] - except KeyError: - # senderId isn't there, which means it hasn't been scanned yet. - return - - for path, changes in items.items(): - try: - v = unwrap_dbus_value(changes['Value']) - except (KeyError, TypeError): - continue - - try: - t = changes['Text'] - except KeyError: - t = str(v) - self._handler_value_changes(service, path, v, t) - - def handler_value_changes(self, changes, path, senderId): - # If this properyChange does not involve a value, our work is done. - if 'Value' not in changes: - return - - try: - service = self.servicesById[senderId] - except KeyError: - # senderId isn't there, which means it hasn't been scanned yet. - return - - v = unwrap_dbus_value(changes['Value']) - # Some services don't send Text with their PropertiesChanged events. - try: - t = changes['Text'] - except KeyError: - t = str(v) - self._handler_value_changes(service, path, v, t) - - def _handler_value_changes(self, service, path, value, text): - try: - a = service.paths[path] - except KeyError: - # path isn't there, which means it hasn't been scanned yet. - return - - service.set_seen(path) - - # First update our store to the new value - if a.value == value: - return - - a.value = value - a.text = text - - # And do the rest of the processing in on the mainloop - if self.valueChangedCallback is not None: - GLib.idle_add(exit_on_error, self._execute_value_changes, service.name, path, { - 'Value': value, 'Text': text}, a.options) - - def _execute_value_changes(self, serviceName, objectPath, changes, options): - # double check that the service still exists, as it might have - # disappeared between scheduling-for and executing this function. - if serviceName not in self.servicesByName: - return - - self.valueChangedCallback(serviceName, objectPath, - options, changes, self.get_device_instance(serviceName)) - - # Gets the value for a certain servicename and path - # The default_value is returned when: - # 1. When the service doesn't exist. - # 2. When the path asked for isn't being monitored. - # 3. When the path exists, but has dbus-invalid, ie an empty byte array. - # 4. When the path asked for is being monitored, but doesn't exist for that service. - def get_value(self, serviceName, objectPath, default_value=None): - service = self.servicesByName.get(serviceName, None) - if service is None: - return default_value - - value = service.paths.get(objectPath, None) - if value is None or value.value is None: - return default_value - - return value.value - - # returns if a dbus exists now, by doing a blocking dbus call. - # Typically seen will be sufficient and doesn't need access to the dbus. - def exists(self, serviceName, objectPath): - try: - self.dbusConn.call_blocking(serviceName, objectPath, None, 'GetValue', '', []) - return True - except dbus.exceptions.DBusException as e: - return False - - # Returns if there ever was a successful GetValue or valueChanged event. - # Unlike get_value this return True also if the actual value is invalid. - # - # Note: the path might no longer exists anymore, but that doesn't happen in - # practice. If a service really wants to reconfigure itself typically it should - # reconnect to the dbus which causes it to be rescanned and seen will be updated. - # If it is really needed to know if a path still exists, use exists. - def seen(self, serviceName, objectPath): - try: - return self.servicesByName[serviceName].seen(objectPath) - except KeyError: - return False - - # Sets the value for a certain servicename and path, returns the return value of the D-Bus SetValue - # method. If the underlying item does not exist (the service does not exist, or the objectPath was not - # registered) the function will return -1 - def set_value(self, serviceName, objectPath, value): - # Check if the D-Bus object referenced by serviceName and objectPath is registered. There is no - # necessity to do this, but it is in line with previous implementations which kept VeDbusItemImport - # objects for registers items only. - service = self.servicesByName.get(serviceName, None) - if service is None: - return -1 - if objectPath not in service.paths: - return -1 - # We do not catch D-Bus exceptions here, because the previous implementation did not do that either. - return self.dbusConn.call_blocking(serviceName, objectPath, - dbus_interface='com.victronenergy.BusItem', - method='SetValue', signature=None, - args=[wrap_dbus_value(value)]) - - # Similar to set_value, but operates asynchronously - def set_value_async(self, serviceName, objectPath, value, - reply_handler=None, error_handler=None): - service = self.servicesByName.get(serviceName, None) - if service is not None: - if objectPath in service.paths: - self.dbusConn.call_async(serviceName, objectPath, - dbus_interface='com.victronenergy.BusItem', - method='SetValue', signature=None, - args=[wrap_dbus_value(value)], - reply_handler=reply_handler, error_handler=error_handler) - return - - if error_handler is not None: - error_handler(TypeError('Service or path not found, ' - 'service=%s, path=%s' % (serviceName, objectPath))) - - # returns a dictionary, keys are the servicenames, value the instances - # optionally use the classfilter to get only a certain type of services, for - # example com.victronenergy.battery. - def get_service_list(self, classfilter=None): - if classfilter is None: - return { servicename: service.deviceInstance \ - for servicename, service in self.servicesByName.items() } - - if classfilter not in self.servicesByClass: - return {} - - return { service.name: service.deviceInstance \ - for service in self.servicesByClass[classfilter] } - - def get_device_instance(self, serviceName): - return self.servicesByName[serviceName].deviceInstance - - def track_value(self, serviceName, objectPath, callback, *args, **kwargs): - """ A DbusMonitor can watch specific service/path combos for changes - so that it is not fully reliant on the global handler_value_changes - in this class. Additional watches are deleted automatically when - the service disappears from dbus. """ - cb = partial(callback, *args, **kwargs) - - def root_tracker(items): - # Check if objectPath in dict - try: - v = items[objectPath] - _v = unwrap_dbus_value(v['Value']) - except (KeyError, TypeError): - return # not in this dict - - try: - t = v['Text'] - except KeyError: - cb({'Value': _v }) - else: - cb({'Value': _v, 'Text': t}) - - # Track changes on the path, and also on root - self.serviceWatches[serviceName].extend(( - self.dbusConn.add_signal_receiver(cb, - dbus_interface='com.victronenergy.BusItem', - signal_name='PropertiesChanged', - path=objectPath, bus_name=serviceName), - self.dbusConn.add_signal_receiver(root_tracker, - dbus_interface='com.victronenergy.BusItem', - signal_name='ItemsChanged', - path="/", bus_name=serviceName), - )) - - -# ====== ALL CODE BELOW THIS LINE IS PURELY FOR DEVELOPING THIS CLASS ====== - -# Example function that can be used as a starting point to use this code -def value_changed_on_dbus(dbusServiceName, dbusPath, dict, changes, deviceInstance): - logger.debug("0 ----------------") - logger.debug("1 %s%s changed" % (dbusServiceName, dbusPath)) - logger.debug("2 vrm dict : %s" % dict) - logger.debug("3 changes-text: %s" % changes['Text']) - logger.debug("4 changes-value: %s" % changes['Value']) - logger.debug("5 deviceInstance: %s" % deviceInstance) - logger.debug("6 - end") - - -def nameownerchange(a, b): - # used to find memory leaks in dbusmonitor and VeDbusItemImport - import gc - gc.collect() - objects = gc.get_objects() - print (len([o for o in objects if type(o).__name__ == 'VeDbusItemImport'])) - print (len([o for o in objects if type(o).__name__ == 'SignalMatch'])) - print (len(objects)) - - -def print_values(dbusmonitor): - a = dbusmonitor.get_value('wrongservice', '/DbusInvalid', default_value=1000) - b = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/NotInTheMonitorList', default_value=1000) - c = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/DbusInvalid', default_value=1000) - d = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/NonExistingButMonitored', default_value=1000) - - print ("All should be 1000: Wrong Service: %s, NotInTheMonitorList: %s, DbusInvalid: %s, NonExistingButMonitored: %s" % (a, b, c, d)) - return True - -# We have a mainloop, but that is just for developing this code. Normally above class & code is used from -# some other class, such as vrmLogger or the pubsub Implementation. -def main(): - # Init logging - logging.basicConfig(level=logging.DEBUG) - logger.info(__file__ + " is starting up") - - # Have a mainloop, so we can send/receive asynchronous calls to and from dbus - DBusGMainLoop(set_as_default=True) - - import os - import sys - sys.path.insert(1, os.path.join(os.path.dirname(__file__), '../../')) - - dummy = {'code': None, 'whenToLog': 'configChange', 'accessLevel': None} - monitorlist = {'com.victronenergy.dummyservice': { - '/Connected': dummy, - '/ProductName': dummy, - '/Mgmt/Connection': dummy, - '/Dc/0/Voltage': dummy, - '/Dc/0/Current': dummy, - '/Dc/0/Temperature': dummy, - '/Load/I': dummy, - '/FirmwareVersion': dummy, - '/DbusInvalid': dummy, - '/NonExistingButMonitored': dummy}} - - d = DbusMonitor(monitorlist, value_changed_on_dbus, - deviceAddedCallback=nameownerchange, deviceRemovedCallback=nameownerchange) - - GLib.timeout_add(1000, print_values, d) - - # Start and run the mainloop - logger.info("Starting mainloop, responding on only events") - mainloop = GLib.MainLoop() - mainloop.run() - -if __name__ == "__main__": - main() diff --git a/velib_python/velib_python/v3.40~37/oldestVersion b/velib_python/velib_python/v3.40~37/oldestVersion deleted file mode 100644 index 3f6c1a0..0000000 --- a/velib_python/velib_python/v3.40~37/oldestVersion +++ /dev/null @@ -1 +0,0 @@ -v3.00~32 diff --git a/velib_python/velib_python/v3.40~37/settingsdevice.py b/velib_python/velib_python/v3.40~37/settingsdevice.py deleted file mode 100644 index a207e8b..0000000 --- a/velib_python/velib_python/v3.40~37/settingsdevice.py +++ /dev/null @@ -1,118 +0,0 @@ -import dbus -import logging -import time -from functools import partial - -# Local imports -from vedbus import VeDbusItemImport - -## Indexes for the setting dictonary. -PATH = 0 -VALUE = 1 -MINIMUM = 2 -MAXIMUM = 3 -SILENT = 4 - -## The Settings Device class. -# Used by python programs, such as the vrm-logger, to read and write settings they -# need to store on disk. And since these settings might be changed from a different -# source, such as the GUI, the program can pass an eventCallback that will be called -# as soon as some setting is changed. -# -# The settings are stored in flash via the com.victronenergy.settings service on dbus. -# See https://github.com/victronenergy/localsettings for more info. -# -# If there are settings in de supportSettings list which are not yet on the dbus, -# and therefore not yet in the xml file, they will be added through the dbus-addSetting -# interface of com.victronenergy.settings. -class SettingsDevice(object): - ## The constructor processes the tree of dbus-items. - # @param bus the system-dbus object - # @param name the dbus-service-name of the settings dbus service, 'com.victronenergy.settings' - # @param supportedSettings dictionary with all setting-names, and their defaultvalue, min, max and whether - # the setting is silent. The 'silent' entry is optional. If set to true, no changes in the setting will - # be logged by localsettings. - # @param eventCallback function that will be called on changes on any of these settings - # @param timeout Maximum interval to wait for localsettings. An exception is thrown at the end of the - # interval if the localsettings D-Bus service has not appeared yet. - def __init__(self, bus, supportedSettings, eventCallback, name='com.victronenergy.settings', timeout=0): - logging.debug("===== Settings device init starting... =====") - self._bus = bus - self._dbus_name = name - self._eventCallback = eventCallback - self._values = {} # stored the values, used to pass the old value along on a setting change - self._settings = {} - - count = 0 - while True: - if 'com.victronenergy.settings' in self._bus.list_names(): - break - if count == timeout: - raise Exception("The settings service com.victronenergy.settings does not exist!") - count += 1 - logging.info('waiting for settings') - time.sleep(1) - - # Add the items. - self.addSettings(supportedSettings) - - logging.debug("===== Settings device init finished =====") - - def addSettings(self, settings): - for setting, options in settings.items(): - silent = len(options) > SILENT and options[SILENT] - busitem = self.addSetting(options[PATH], options[VALUE], - options[MINIMUM], options[MAXIMUM], silent, callback=partial(self.handleChangedSetting, setting)) - self._settings[setting] = busitem - self._values[setting] = busitem.get_value() - - def addSetting(self, path, value, _min, _max, silent=False, callback=None): - busitem = VeDbusItemImport(self._bus, self._dbus_name, path, callback) - if busitem.exists and (value, _min, _max, silent) == busitem._proxy.GetAttributes(): - logging.debug("Setting %s found" % path) - else: - logging.info("Setting %s does not exist yet or must be adjusted" % path) - - # Prepare to add the setting. Most dbus types extend the python - # type so it is only necessary to additionally test for Int64. - if isinstance(value, (int, dbus.Int64)): - itemType = 'i' - elif isinstance(value, float): - itemType = 'f' - else: - itemType = 's' - - # Add the setting - # TODO, make an object that inherits VeDbusItemImport, and complete the D-Bus settingsitem interface - settings_item = VeDbusItemImport(self._bus, self._dbus_name, '/Settings', createsignal=False) - setting_path = path.replace('/Settings/', '', 1) - if silent: - settings_item._proxy.AddSilentSetting('', setting_path, value, itemType, _min, _max) - else: - settings_item._proxy.AddSetting('', setting_path, value, itemType, _min, _max) - - busitem = VeDbusItemImport(self._bus, self._dbus_name, path, callback) - - return busitem - - def handleChangedSetting(self, setting, servicename, path, changes): - oldvalue = self._values[setting] if setting in self._values else None - self._values[setting] = changes['Value'] - - if self._eventCallback is None: - return - - self._eventCallback(setting, oldvalue, changes['Value']) - - def setDefault(self, path): - item = VeDbusItemImport(self._bus, self._dbus_name, path, createsignal=False) - item.set_default() - - def __getitem__(self, setting): - return self._settings[setting].get_value() - - def __setitem__(self, setting, newvalue): - result = self._settings[setting].set_value(newvalue) - if result != 0: - # Trying to make some false change to our own settings? How dumb! - assert False diff --git a/velib_python/velib_python/v3.40~37/ve_utils.py b/velib_python/velib_python/v3.40~37/ve_utils.py deleted file mode 100644 index 63a915b..0000000 --- a/velib_python/velib_python/v3.40~37/ve_utils.py +++ /dev/null @@ -1,262 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -import sys -from traceback import print_exc -from os import _exit as os_exit -from os import statvfs -from subprocess import check_output, CalledProcessError -import logging -import dbus -logger = logging.getLogger(__name__) - -VEDBUS_INVALID = dbus.Array([], signature=dbus.Signature('i'), variant_level=1) - -class NoVrmPortalIdError(Exception): - pass - -# Use this function to make sure the code quits on an unexpected exception. Make sure to use it -# when using GLib.idle_add and also GLib.timeout_add. -# Without this, the code will just keep running, since GLib does not stop the mainloop on an -# exception. -# Example: GLib.idle_add(exit_on_error, myfunc, arg1, arg2) -def exit_on_error(func, *args, **kwargs): - try: - return func(*args, **kwargs) - except: - try: - print ('exit_on_error: there was an exception. Printing stacktrace will be tried and then exit') - print_exc() - except: - pass - - # sys.exit() is not used, since that throws an exception, which does not lead to a program - # halt when used in a dbus callback, see connection.py in the Python/Dbus libraries, line 230. - os_exit(1) - - -__vrm_portal_id = None -def get_vrm_portal_id(): - # The original definition of the VRM Portal ID is that it is the mac - # address of the onboard- ethernet port (eth0), stripped from its colons - # (:) and lower case. This may however differ between platforms. On Venus - # the task is therefore deferred to /sbin/get-unique-id so that a - # platform specific method can be easily defined. - # - # If /sbin/get-unique-id does not exist, then use the ethernet address - # of eth0. This also handles the case where velib_python is used as a - # package install on a Raspberry Pi. - # - # On a Linux host where the network interface may not be eth0, you can set - # the VRM_IFACE environment variable to the correct name. - - global __vrm_portal_id - - if __vrm_portal_id: - return __vrm_portal_id - - portal_id = None - - # First try the method that works if we don't have a data partition. This - # will fail when the current user is not root. - try: - portal_id = check_output("/sbin/get-unique-id").decode("utf-8", "ignore").strip() - if not portal_id: - raise NoVrmPortalIdError("get-unique-id returned blank") - __vrm_portal_id = portal_id - return portal_id - except CalledProcessError: - # get-unique-id returned non-zero - raise NoVrmPortalIdError("get-unique-id returned non-zero") - except OSError: - # File doesn't exist, use fallback - pass - - # Fall back to getting our id using a syscall. Assume we are on linux. - # Allow the user to override what interface is used using an environment - # variable. - import fcntl, socket, struct, os - - iface = os.environ.get('VRM_IFACE', 'eth0').encode('ascii') - s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - try: - info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', iface[:15])) - except IOError: - raise NoVrmPortalIdError("ioctl failed for eth0") - - __vrm_portal_id = info[18:24].hex() - return __vrm_portal_id - - -# See VE.Can registers - public.docx for definition of this conversion -def convert_vreg_version_to_readable(version): - def str_to_arr(x, length): - a = [] - for i in range(0, len(x), length): - a.append(x[i:i+length]) - return a - - x = "%x" % version - x = x.upper() - - if len(x) == 5 or len(x) == 3 or len(x) == 1: - x = '0' + x - - a = str_to_arr(x, 2); - - # remove the first 00 if there are three bytes and it is 00 - if len(a) == 3 and a[0] == '00': - a.remove(0); - - # if we have two or three bytes now, and the first character is a 0, remove it - if len(a) >= 2 and a[0][0:1] == '0': - a[0] = a[0][1]; - - result = '' - for item in a: - result += ('.' if result != '' else '') + item - - - result = 'v' + result - - return result - - -def get_free_space(path): - result = -1 - - try: - s = statvfs(path) - result = s.f_frsize * s.f_bavail # Number of free bytes that ordinary users - except Exception as ex: - logger.info("Error while retrieving free space for path %s: %s" % (path, ex)) - - return result - - -def _get_sysfs_machine_name(): - try: - with open('/sys/firmware/devicetree/base/model', 'r') as f: - return f.read().rstrip('\x00') - except IOError: - pass - - return None - -# Returns None if it cannot find a machine name. Otherwise returns the string -# containing the name -def get_machine_name(): - # First try calling the venus utility script - try: - return check_output("/usr/bin/product-name").strip().decode('UTF-8') - except (CalledProcessError, OSError): - pass - - # Fall back to sysfs - name = _get_sysfs_machine_name() - if name is not None: - return name - - # Fall back to venus build machine name - try: - with open('/etc/venus/machine', 'r', encoding='UTF-8') as f: - return f.read().strip() - except IOError: - pass - - return None - - -def get_product_id(): - """ Find the machine ID and return it. """ - - # First try calling the venus utility script - try: - return check_output("/usr/bin/product-id").strip().decode('UTF-8') - except (CalledProcessError, OSError): - pass - - # Fall back machine name mechanism - name = _get_sysfs_machine_name() - return { - 'Color Control GX': 'C001', - 'Venus GX': 'C002', - 'Octo GX': 'C006', - 'EasySolar-II': 'C007', - 'MultiPlus-II': 'C008', - 'Maxi GX': 'C009', - 'Cerbo GX': 'C00A' - }.get(name, 'C003') # C003 is Generic - - -# Returns False if it cannot open the file. Otherwise returns its rstripped contents -def read_file(path): - content = False - - try: - with open(path, 'r') as f: - content = f.read().rstrip() - except Exception as ex: - logger.debug("Error while reading %s: %s" % (path, ex)) - - return content - - -def wrap_dbus_value(value): - if value is None: - return VEDBUS_INVALID - if isinstance(value, float): - return dbus.Double(value, variant_level=1) - if isinstance(value, bool): - return dbus.Boolean(value, variant_level=1) - if isinstance(value, int): - try: - return dbus.Int32(value, variant_level=1) - except OverflowError: - return dbus.Int64(value, variant_level=1) - if isinstance(value, str): - return dbus.String(value, variant_level=1) - if isinstance(value, list): - if len(value) == 0: - # If the list is empty we cannot infer the type of the contents. So assume unsigned integer. - # A (signed) integer is dangerous, because an empty list of signed integers is used to encode - # an invalid value. - return dbus.Array([], signature=dbus.Signature('u'), variant_level=1) - return dbus.Array([wrap_dbus_value(x) for x in value], variant_level=1) - if isinstance(value, dict): - # Wrapping the keys of the dictionary causes D-Bus errors like: - # 'arguments to dbus_message_iter_open_container() were incorrect, - # assertion "(type == DBUS_TYPE_ARRAY && contained_signature && - # *contained_signature == DBUS_DICT_ENTRY_BEGIN_CHAR) || (contained_signature == NULL || - # _dbus_check_is_valid_signature (contained_signature))" failed in file ...' - return dbus.Dictionary({(k, wrap_dbus_value(v)) for k, v in value.items()}, variant_level=1) - return value - - -dbus_int_types = (dbus.Int32, dbus.UInt32, dbus.Byte, dbus.Int16, dbus.UInt16, dbus.UInt32, dbus.Int64, dbus.UInt64) - - -def unwrap_dbus_value(val): - """Converts D-Bus values back to the original type. For example if val is of type DBus.Double, - a float will be returned.""" - if isinstance(val, dbus_int_types): - return int(val) - if isinstance(val, dbus.Double): - return float(val) - if isinstance(val, dbus.Array): - v = [unwrap_dbus_value(x) for x in val] - return None if len(v) == 0 else v - if isinstance(val, (dbus.Signature, dbus.String)): - return str(val) - # Python has no byte type, so we convert to an integer. - if isinstance(val, dbus.Byte): - return int(val) - if isinstance(val, dbus.ByteArray): - return "".join([bytes(x) for x in val]) - if isinstance(val, (list, tuple)): - return [unwrap_dbus_value(x) for x in val] - if isinstance(val, (dbus.Dictionary, dict)): - # Do not unwrap the keys, see comment in wrap_dbus_value - return dict([(x, unwrap_dbus_value(y)) for x, y in val.items()]) - if isinstance(val, dbus.Boolean): - return bool(val) - return val diff --git a/velib_python/velib_python/v3.40~37/vedbus.py b/velib_python/velib_python/v3.40~37/vedbus.py deleted file mode 100644 index 8c101ea..0000000 --- a/velib_python/velib_python/v3.40~37/vedbus.py +++ /dev/null @@ -1,611 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import dbus.service -import logging -import traceback -import os -import weakref -from collections import defaultdict -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 = {} - self._ratelimiters = [] - self._dbusname = None - - # 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['/'] = VeDbusRootExport(self._dbusconn, '/', self) - - logging.info("registered ourselves on D-Bus as %s" % servicename) - - # To force immediate deregistering of this dbus service and all its object paths, explicitly - # call __del__(). - def __del__(self): - for node in list(self._dbusnodes.values()): - node.__del__() - self._dbusnodes.clear() - for item in list(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, valuetype=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, valuetype=valuetype) - - 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] = VeDbusTreeExport(self._dbusconn, subPath, self) - 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) - - # 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 list(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 - - def __enter__(self): - l = ServiceContext(self) - self._ratelimiters.append(l) - return l - - def __exit__(self, *exc): - # pop off the top one and flush it. If with statements are nested - # then each exit flushes its own part. - if self._ratelimiters: - self._ratelimiters.pop().flush() - -class ServiceContext(object): - def __init__(self, parent): - self.parent = parent - self.changes = {} - - def __getitem__(self, path): - return self.parent[path] - - def __setitem__(self, path, newvalue): - c = self.parent._dbusobjects[path]._local_set_value(newvalue) - if c is not None: - self.changes[path] = c - - def flush(self): - if self.changes: - self.parent._dbusnodes['/'].ItemsChanged(self.changes) - -class TrackerDict(defaultdict): - """ Same as defaultdict, but passes the key to default_factory. """ - def __missing__(self, key): - self[key] = x = self.default_factory(key) - return x - -class VeDbusRootTracker(object): - """ This tracks the root of a dbus path and listens for PropertiesChanged - signals. When a signal arrives, parse it and unpack the key/value changes - into traditional events, then pass it to the original eventCallback - method. """ - def __init__(self, bus, serviceName): - self.importers = defaultdict(weakref.WeakSet) - self.serviceName = serviceName - self._match = bus.get_object(serviceName, '/', introspect=False).connect_to_signal( - "ItemsChanged", weak_functor(self._items_changed_handler)) - - def __del__(self): - self._match.remove() - self._match = None - - def add(self, i): - self.importers[i.path].add(i) - - def _items_changed_handler(self, items): - if not isinstance(items, dict): - return - - for path, changes in items.items(): - try: - v = changes['Value'] - except KeyError: - continue - - try: - t = changes['Text'] - except KeyError: - t = str(unwrap_dbus_value(v)) - - for i in self.importers.get(path, ()): - i._properties_changed_handler({'Value': v, 'Text': t}) - -""" -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): - def __new__(cls, bus, serviceName, path, eventCallback=None, createsignal=True): - instance = object.__new__(cls) - - # If signal tracking should be done, also add to root tracker - if createsignal: - if "_roots" not in cls.__dict__: - cls._roots = TrackerDict(lambda k: VeDbusRootTracker(bus, k)) - - return instance - - ## 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)) - self._roots[serviceName].add(self) - - # 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 is not 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 - - ## Resets the item to its default value - def set_default(self): - self._proxy.SetDefault() - self._refreshcachedvalue() - - ## 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, service): - dbus.service.Object.__init__(self, bus, objectPath) - self._service = service - 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] - - def _get_value_handler(self, path, get_text=False): - logging.debug("_get_value_handler called for %s" % path) - r = {} - px = path - if not px.endswith('/'): - px += '/' - for p, item in self._service._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 - - @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 VeDbusRootExport(VeDbusTreeExport): - @dbus.service.signal('com.victronenergy.BusItem', signature='a{sa{sv}}') - def ItemsChanged(self, changes): - pass - - @dbus.service.method('com.victronenergy.BusItem', out_signature='a{sa{sv}}') - def GetItems(self): - return { - path: { - 'Value': wrap_dbus_value(item.local_get_value()), - 'Text': item.GetText() } - for path, item in self._service._dbusobjects.items() - } - - -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, - valuetype=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 - self._type = valuetype - - # 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.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): - changes = self._local_set_value(newvalue) - if changes is not None: - self.PropertiesChanged(changes) - - def _local_set_value(self, newvalue): - if self._value == newvalue: - return None - - self._value = newvalue - return { - 'Value': wrap_dbus_value(newvalue), - 'Text': self.GetText() - } - - 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 value type is enforced, cast it. If the type can be coerced - # python will do it for us. This allows ints to become floats, - # or bools to become ints. Additionally also allow None, so that - # a path may be invalidated. - if self._type is not None and newvalue is not None: - try: - newvalue = self._type(newvalue) - except (ValueError, TypeError): - return 1 # NOT OK - - 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) diff --git a/venus-data-UninstallPackages.tgz b/venus-data-UninstallPackages.tgz index f55c4d1..3d3fae2 100644 Binary files a/venus-data-UninstallPackages.tgz and b/venus-data-UninstallPackages.tgz differ diff --git a/venus-data.tgz b/venus-data.tgz index 0541a5c..ef0954a 100644 Binary files a/venus-data.tgz and b/venus-data.tgz differ diff --git a/version b/version index 1416d7e..5eb381b 100644 --- a/version +++ b/version @@ -1 +1 @@ -v8.9 +v8.10