diff --git a/FileSets/PatchSource/PageSettings.qml b/FileSets/PatchSource/PageSettings.qml index e85d33c..c25179f 100644 --- a/FileSets/PatchSource/PageSettings.qml +++ b/FileSets/PatchSource/PageSettings.qml @@ -1,6 +1,3 @@ -//////// modified to insert PackageManager menu -//////// auto-generated by SetupHelper setup script - import QtQuick 1.1 import com.victron.velib 1.0 import net.connman 0.1 diff --git a/FileSets/PatchSource/PageSettings.qml.patch b/FileSets/PatchSource/PageSettings.qml.patch index 80ec615..7c0cdd9 100644 --- a/FileSets/PatchSource/PageSettings.qml.patch +++ b/FileSets/PatchSource/PageSettings.qml.patch @@ -1,13 +1,6 @@ --- /Users/Kevin/GitHub/SetupHelper.copy/FileSets/PatchSource/PageSettings.qml.orig 2024-05-15 13:06:53 -+++ /Users/Kevin/GitHub/SetupHelper.copy/FileSets/PatchSource/PageSettings.qml 2024-05-15 13:06:53 -@@ -1,3 +1,6 @@ -+//////// modified to insert PackageManager menu -+//////// auto-generated by SetupHelper setup script -+ - import QtQuick 1.1 - import com.victron.velib 1.0 - import net.connman 0.1 -@@ -192,5 +195,11 @@ ++++ /Users/Kevin/GitHub/SetupHelper.copy/FileSets/PatchSource/PageSettings.qml 2025-01-24 22:39:59 +@@ -192,5 +192,11 @@ subpage: Component { PageDebug {} } showAccessLevel: User.AccessService } diff --git a/HelperResources/CommonResources b/HelperResources/CommonResources index 541fe0c..c4c0ed2 100755 --- a/HelperResources/CommonResources +++ b/HelperResources/CommonResources @@ -1536,6 +1536,20 @@ if [ $scriptAction != 'UNINSTALL' ]; then done fi + # create patched files for all qml files for the change to QtQuick 2 + versionStringToNumber "v3.60~18" + if (( $venusVersionNumber >= $versionNumber )); then + logMessage "patching QtQuick 1.1 to QtQuick 2 in all .qml replacements" + for file in ${fileListVersionIndependent[@]}; do + baseName=$( basename "$file" ) + if ! [[ "$baseName" == *.qml ]]; then continue; fi + sourceFile="$versionIndependentFileSet/$baseName" + if ! [ -f "$sourceFile" ]; then continue; fi + if (( $(grep -c "QtQuick 1.1" "$sourceFile") == 0 )); then continue; fi + sed -e 's/QtQuick 1.1/QtQuick 2/' "$sourceFile" > "$tempFileDir/$baseName" + done + fi + # create the forward and reverse patched files # used during the actual install and to test if the patch/reverse patch will succeed # done here so PackageManager knows if this will be possible before starting the install diff --git a/blindInstall/SetupHelperVersion b/blindInstall/SetupHelperVersion index 5dee719..4798d53 100644 --- a/blindInstall/SetupHelperVersion +++ b/blindInstall/SetupHelperVersion @@ -1 +1 @@ -v8.26 +v8.27~1 diff --git a/changes b/changes index 8455943..9666864 100644 --- a/changes +++ b/changes @@ -1,3 +1,6 @@ +v8.27: + support the change to QtQuick 2 first used in v3.60~18 + v8.26: added link to IncludeHelpers so that old packages can find correct file to source diff --git a/velib_python/velib_python/latest/dbusmonitor.py b/velib_python/velib_python/latest/dbusmonitor.py new file mode 100644 index 0000000..46b1dd8 --- /dev/null +++ b/velib_python/velib_python/latest/dbusmonitor.py @@ -0,0 +1,592 @@ +#!/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 + +# dbus interface +VE_INTERFACE = "com.victronenergy.BusItem" + +# For lookups where None is a valid result +notfound = object() + +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, '/', VE_INTERFACE, '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', VE_INTERFACE, '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, '/', VE_INTERFACE, 'GetValue', '', [])) + texts.update(self.dbusConn.call_blocking(serviceName, '/', VE_INTERFACE, '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, VE_INTERFACE, 'GetValue', '', []) + service.set_seen(path) + text = self.dbusConn.call_blocking(serviceName, path, VE_INTERFACE, '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, VE_INTERFACE, '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=VE_INTERFACE, + 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=VE_INTERFACE, + 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 new file mode 100644 index 0000000..42a23a2 --- /dev/null +++ b/velib_python/velib_python/latest/oldestVersion @@ -0,0 +1 @@ +v3.50 diff --git a/velib_python/velib_python/latest/settingsdevice.py b/velib_python/velib_python/latest/settingsdevice.py new file mode 100644 index 0000000..a207e8b --- /dev/null +++ b/velib_python/velib_python/latest/settingsdevice.py @@ -0,0 +1,118 @@ +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 new file mode 100644 index 0000000..f5a2f85 --- /dev/null +++ b/velib_python/velib_python/latest/ve_utils.py @@ -0,0 +1,276 @@ +#!/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 new file mode 100644 index 0000000..7b02e62 --- /dev/null +++ b/velib_python/velib_python/latest/vedbus.py @@ -0,0 +1,650 @@ +#!/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: + logging.warning("USING OUTDATED REGISTRATION METHOD!") + logging.warning("Please set register=False, then call the register method " + "after adding all mandatory paths. See " + "https://github.com/victronenergy/venus/wiki/dbus-api") + 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/v3.34/dbusmonitor.py b/velib_python/velib_python/v3.34/dbusmonitor.py new file mode 100644 index 0000000..cb2185d --- /dev/null +++ b/velib_python/velib_python/v3.34/dbusmonitor.py @@ -0,0 +1,554 @@ +#!/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.34/oldestVersion b/velib_python/velib_python/v3.34/oldestVersion new file mode 100644 index 0000000..1d17edb --- /dev/null +++ b/velib_python/velib_python/v3.34/oldestVersion @@ -0,0 +1 @@ +v3.10 diff --git a/velib_python/velib_python/v3.34/settingsdevice.py b/velib_python/velib_python/v3.34/settingsdevice.py new file mode 100644 index 0000000..a207e8b --- /dev/null +++ b/velib_python/velib_python/v3.34/settingsdevice.py @@ -0,0 +1,118 @@ +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.34/ve_utils.py b/velib_python/velib_python/v3.34/ve_utils.py new file mode 100644 index 0000000..63a915b --- /dev/null +++ b/velib_python/velib_python/v3.34/ve_utils.py @@ -0,0 +1,262 @@ +#!/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.34/vedbus.py b/velib_python/velib_python/v3.34/vedbus.py new file mode 100644 index 0000000..8c101ea --- /dev/null +++ b/velib_python/velib_python/v3.34/vedbus.py @@ -0,0 +1,611 @@ +#!/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/velib_python/v3.41/dbusmonitor.py b/velib_python/velib_python/v3.41/dbusmonitor.py new file mode 100644 index 0000000..fd25700 --- /dev/null +++ b/velib_python/velib_python/v3.41/dbusmonitor.py @@ -0,0 +1,587 @@ +#!/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/v3.41/oldestVersion b/velib_python/velib_python/v3.41/oldestVersion new file mode 100644 index 0000000..c967d38 --- /dev/null +++ b/velib_python/velib_python/v3.41/oldestVersion @@ -0,0 +1 @@ +v3.40 diff --git a/velib_python/velib_python/v3.41/settingsdevice.py b/velib_python/velib_python/v3.41/settingsdevice.py new file mode 100644 index 0000000..a207e8b --- /dev/null +++ b/velib_python/velib_python/v3.41/settingsdevice.py @@ -0,0 +1,118 @@ +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.41/ve_utils.py b/velib_python/velib_python/v3.41/ve_utils.py new file mode 100644 index 0000000..f5a2f85 --- /dev/null +++ b/velib_python/velib_python/v3.41/ve_utils.py @@ -0,0 +1,276 @@ +#!/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/v3.41/vedbus.py b/velib_python/velib_python/v3.41/vedbus.py new file mode 100644 index 0000000..cb95ba1 --- /dev/null +++ b/velib_python/velib_python/v3.41/vedbus.py @@ -0,0 +1,646 @@ +#!/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/venus-data-UninstallPackages.tgz b/venus-data-UninstallPackages.tgz index 8ef1493..a0b0bb5 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 9979feb..9ca2ebc 100644 Binary files a/venus-data.tgz and b/venus-data.tgz differ diff --git a/version b/version index 5dee719..abe53de 100644 --- a/version +++ b/version @@ -1 +1 @@ -v8.26 +v8.27