From 5ecc8f23d150f5eb628d74ad05208a27b651df38 Mon Sep 17 00:00:00 2001 From: Manuel Date: Wed, 8 Mar 2023 11:16:46 +0100 Subject: [PATCH] Fixed system calculations Now all needed values for dbus-systemcalc-py are provided --- LICENSE | 21 + .../dbus-multiplus-emulator.py | 158 ++++- .../ext/velib_python/dbusmonitor.py | 554 ++++++++++++++++++ dbus-multiplus-emulator/install.sh | 8 + dbus-spy.txt | 246 -------- 5 files changed, 727 insertions(+), 260 deletions(-) create mode 100644 LICENSE create mode 100644 dbus-multiplus-emulator/ext/velib_python/dbusmonitor.py delete mode 100644 dbus-spy.txt diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bc91dd2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Manuel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/dbus-multiplus-emulator/dbus-multiplus-emulator.py b/dbus-multiplus-emulator/dbus-multiplus-emulator.py index 1b3aa4f..21271e9 100644 --- a/dbus-multiplus-emulator/dbus-multiplus-emulator.py +++ b/dbus-multiplus-emulator/dbus-multiplus-emulator.py @@ -10,12 +10,38 @@ # import Victron Energy packages sys.path.insert(1, os.path.join(os.path.dirname(__file__), 'ext', 'velib_python')) from vedbus import VeDbusService +from dbusmonitor import DbusMonitor # use WARNING for default, INFO for displaying actual steps and values, DEBUG for debugging logging.basicConfig(level=logging.WARNING) + +# enter grid frequency +grid_frequency = 50.0000 + +# enter the dbusServiceName from which the battery data should be fetched +dbusServiceNameBattery = 'com.victronenergy.battery.zero' + +# enter the dbusServiceName from which the grid meter data should be fetched +dbusServiceNameGrid = 'com.victronenergy.grid.mqtt_grid' + + + class DbusMultiPlusEmulator: + + # create dummy until updated + batteryValues = { + '/Dc/0/Current': 0, + '/Dc/0/Power': 0, + '/Dc/0/Temperature': 0, + '/Dc/0/Voltage': 0, + '/Soc': 0 + } + gridValues = { + '/Ac/Voltage': 230 + } + def __init__( self, servicename, @@ -54,21 +80,121 @@ def __init__( path, settings['initial'], writeable=True, onchangecallback=self._handlechangedvalue ) + + ## read values from battery + # Why this dummy? Because DbusMonitor expects these values to be there, even though we don't + # need them. So just add some dummy data. This can go away when DbusMonitor is more generic. + dummy = {'code': None, 'whenToLog': 'configChange', 'accessLevel': None} + dbus_tree = { + 'com.victronenergy.battery': { + '/Connected': dummy, + '/ProductName': dummy, + '/Mgmt/Connection': dummy, + '/DeviceInstance': dummy, + '/Dc/0/Current': dummy, + '/Dc/0/Power': dummy, + '/Dc/0/Temperature': dummy, + '/Dc/0/Voltage': dummy, + '/Soc': dummy, + #'/Sense/Current': dummy, + #'/TimeToGo': dummy, + #'/ConsumedAmphours': dummy, + #'/ProductId': dummy, + #'/CustomName': dummy, + #'/Info/MaxChargeVoltage': dummy + }, + 'com.victronenergy.grid' : { + # '/Connected': dummy, + # '/ProductName': dummy, + # '/Mgmt/Connection': dummy, + # '/ProductId' : dummy, + # '/DeviceType' : dummy, + # '/Ac/L1/Power': dummy, + # '/Ac/L2/Power': dummy, + # '/Ac/L3/Power': dummy, + # '/Ac/L1/Current': dummy, + # '/Ac/L2/Current': dummy, + # '/Ac/L3/Current': dummy + '/Ac/Voltage': dummy + }, + } + + #self._dbusreadservice = DbusMonitor('com.victronenergy.battery.zero') + self._dbusmonitor = self._create_dbus_monitor( + dbus_tree, + valueChangedCallback=self._dbus_value_changed, + deviceAddedCallback=self._device_added, + deviceRemovedCallback=self._device_removed + ) + GLib.timeout_add(1000, self._update) # pause 1000ms before the next request + + def _create_dbus_monitor(self, *args, **kwargs): + return DbusMonitor(*args, **kwargs) + + + def _dbus_value_changed(self, dbusServiceName, dbusPath, dict, changes, deviceInstance): + self._changed = True + + if dbusServiceName == dbusServiceNameBattery: + self.batteryValues.update({ + str(dbusPath): changes['Value'] + }) + + if dbusServiceName == dbusServiceNameGrid: + self.gridValues.update({ + str(dbusPath): changes['Value'] + }) + + #print('_dbus_value_changed') + #print(dbusServiceName) + #print(dbusPath) + #print(dict) + #print(changes) + #print(deviceInstance) + + #print(self.batteryValues) + #print(self.gridValues) + + def _device_added(self, service, instance, do_service_change=True): + + #print('_device_added') + #print(service) + #print(instance) + #print(do_service_change) + + pass + + def _device_removed(self, service, instance): + + #print('_device_added') + #print(service) + #print(instance) + + pass + def _update(self): + ac = { + 'current': round(self.batteryValues['/Dc/0/Power']/self.gridValues['/Ac/Voltage']), + 'power': self.batteryValues['/Dc/0/Power'], + 'voltage': self.gridValues['/Ac/Voltage'] + } + self._dbusservice['/Ac/ActiveIn/ActiveInput'] = 0 self._dbusservice['/Ac/ActiveIn/Connected'] = 1 self._dbusservice['/Ac/ActiveIn/CurrentLimit'] = 16 self._dbusservice['/Ac/ActiveIn/CurrentLimitIsAdjustable'] = 1 - self._dbusservice['/Ac/ActiveIn/L1/F'] = 50.1111 - self._dbusservice['/Ac/ActiveIn/L1/I'] = 0 - self._dbusservice['/Ac/ActiveIn/L1/P'] = 0 - self._dbusservice['/Ac/ActiveIn/L1/S'] = 0 - self._dbusservice['/Ac/ActiveIn/L1/V'] = 230.33 + # get values from BMS + # for bubble flow in chart and load visualization + self._dbusservice['/Ac/ActiveIn/L1/F'] = grid_frequency + self._dbusservice['/Ac/ActiveIn/L1/I'] = ac['current'] + self._dbusservice['/Ac/ActiveIn/L1/P'] = ac['power'] + self._dbusservice['/Ac/ActiveIn/L1/S'] = ac['power'] + self._dbusservice['/Ac/ActiveIn/L1/V'] = ac['voltage'] self._dbusservice['/Ac/ActiveIn/L2/F'] = None self._dbusservice['/Ac/ActiveIn/L2/I'] = None @@ -82,8 +208,10 @@ def _update(self): self._dbusservice['/Ac/ActiveIn/L3/S'] = None self._dbusservice['/Ac/ActiveIn/L3/V'] = None - self._dbusservice['/Ac/ActiveIn/P'] = 0 - self._dbusservice['/Ac/ActiveIn/S'] = 0 + # get values from BMS + # for bubble flow in chart and load visualization + self._dbusservice['/Ac/ActiveIn/P'] = ac['power'] + self._dbusservice['/Ac/ActiveIn/S'] = ac['power'] self._dbusservice['/Ac/In/1/CurrentLimit'] = 16 self._dbusservice['/Ac/In/1/CurrentLimitIsAdjustable'] = 1 @@ -94,11 +222,11 @@ def _update(self): self._dbusservice['/Ac/NumberOfAcInputs'] = 1 self._dbusservice['/Ac/NumberOfPhases'] = 1 - self._dbusservice['/Ac/Out/L1/F'] = 50.1111 + self._dbusservice['/Ac/Out/L1/F'] = grid_frequency self._dbusservice['/Ac/Out/L1/I'] = 0 self._dbusservice['/Ac/Out/L1/P'] = 0 self._dbusservice['/Ac/Out/L1/S'] = 0 - self._dbusservice['/Ac/Out/L1/V'] = 230.33 + self._dbusservice['/Ac/Out/L1/V'] = ac['voltage'] self._dbusservice['/Ac/Out/L2/F'] = None self._dbusservice['/Ac/Out/L2/I'] = None @@ -156,11 +284,13 @@ def _update(self): self._dbusservice['/Bms/Error'] = 0 self._dbusservice['/Bms/PreAlarm'] = None - self._dbusservice['/Dc/0/Current'] = 0 + # get values from BMS + # for bubble flow in GUI + self._dbusservice['/Dc/0/Current'] = self.batteryValues['/Dc/0/Current'] self._dbusservice['/Dc/0/MaxChargeCurrent'] = 70 - self._dbusservice['/Dc/0/Power'] = 0 - self._dbusservice['/Dc/0/Temperature'] = 0 - self._dbusservice['/Dc/0/Voltage'] = 0 + self._dbusservice['/Dc/0/Power'] = self.batteryValues['/Dc/0/Power'] + self._dbusservice['/Dc/0/Temperature'] = self.batteryValues['/Dc/0/Temperature'] + self._dbusservice['/Dc/0/Voltage'] = self.batteryValues['/Dc/0/Voltage'] #self._dbusservice['/Devices/0/Assistants'] = 0 @@ -261,7 +391,7 @@ def _update(self): self._dbusservice['/Settings/SystemSetup/AcInput1'] = 1 self._dbusservice['/Settings/SystemSetup/AcInput2'] = 0 self._dbusservice['/ShortIds'] = 1 - self._dbusservice['/Soc'] = 0 + self._dbusservice['/Soc'] = self.batteryValues['/Soc'] self._dbusservice['/State'] = 8 self._dbusservice['/SystemReset'] = None self._dbusservice['/VebusChargeState'] = 1 diff --git a/dbus-multiplus-emulator/ext/velib_python/dbusmonitor.py b/dbus-multiplus-emulator/ext/velib_python/dbusmonitor.py new file mode 100644 index 0000000..cb2185d --- /dev/null +++ b/dbus-multiplus-emulator/ext/velib_python/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/dbus-multiplus-emulator/install.sh b/dbus-multiplus-emulator/install.sh index 2073bf1..70ecc54 100644 --- a/dbus-multiplus-emulator/install.sh +++ b/dbus-multiplus-emulator/install.sh @@ -25,3 +25,11 @@ fi # if not alreay added, then add to rc.local grep -qxF "bash $SCRIPT_DIR/install.sh" $filename || echo "bash $SCRIPT_DIR/install.sh" >> $filename + +# set needed dbus settings +dbus -y com.victronenergy.settings /Settings AddSetting Alarm/System GridLost 1 i 0 2 +dbus -y com.victronenergy.settings /Settings AddSetting CanBms/SocketcanCan0 CustomName '' s 0 2 +dbus -y com.victronenergy.settings /Settings AddSetting CanBms/SocketcanCan0 ProductId 0 i 0 9999 +dbus -y com.victronenergy.settings /Settings AddSetting Canbus/can0 Profile 0 i 0 9999 +dbus -y com.victronenergy.settings /Settings AddSetting SystemSetup AcInput1 1 i 0 2 +dbus -y com.victronenergy.settings /Settings AddSetting SystemSetup AcInput2 0 i 0 2 diff --git a/dbus-spy.txt b/dbus-spy.txt deleted file mode 100644 index 8e021e7..0000000 --- a/dbus-spy.txt +++ /dev/null @@ -1,246 +0,0 @@ -Ac/ActiveIn/ActiveInput 0 -Ac/ActiveIn/Connected 1 -Ac/ActiveIn/CurrentLimit 16 -Ac/ActiveIn/CurrentLimitIsAdjustable 1 -Ac/ActiveIn/L1/F 50.1026 -Ac/ActiveIn/L1/I -2.24 -Ac/ActiveIn/L1/P -442 -Ac/ActiveIn/L1/S -524 -Ac/ActiveIn/L1/V 233.95 -Ac/ActiveIn/L2/F - -Ac/ActiveIn/L2/I - -Ac/ActiveIn/L2/P - -Ac/ActiveIn/L2/S - -Ac/ActiveIn/L2/V - -Ac/ActiveIn/L3/F - -Ac/ActiveIn/L3/I - -Ac/ActiveIn/L3/P - -Ac/ActiveIn/L3/S - -Ac/ActiveIn/L3/V - -Ac/ActiveIn/P -442 -Ac/ActiveIn/S -524 -Ac/In/1/CurrentLimit 16 -Ac/In/1/CurrentLimitIsAdjustable 1 -Ac/In/2/CurrentLimit - -Ac/In/2/CurrentLimitIsAdjustable - -Ac/NumberOfAcInputs 1 -Ac/NumberOfPhases 1 -Ac/Out/L1/F 49.9488 -Ac/Out/L1/I 0.06 -Ac/Out/L1/P 26 -Ac/Out/L1/S 14 -Ac/Out/L1/V 233.95 -Ac/Out/L2/F - -Ac/Out/L2/I - -Ac/Out/L2/P - -Ac/Out/L2/S - -Ac/Out/L2/V - -Ac/Out/L3/F - -Ac/Out/L3/I - -Ac/Out/L3/P - -Ac/Out/L3/S - -Ac/Out/L3/V - -Ac/Out/P 26 -Ac/Out/S 14 -Ac/PowerMeasurementType 4 -Ac/State/IgnoreAcIn1 0 -Ac/State/SplitPhaseL2Passthru - -AcSensor/0/Current - -AcSensor/0/Energy - -AcSensor/0/Location - -AcSensor/0/Phase - -AcSensor/0/Power - -AcSensor/0/Voltage - -AcSensor/1/Current - -AcSensor/1/Energy - -AcSensor/1/Location - -AcSensor/1/Phase - -AcSensor/1/Power - -AcSensor/1/Voltage - -AcSensor/2/Current - -AcSensor/2/Energy - -AcSensor/2/Location - -AcSensor/2/Phase - -AcSensor/2/Power - -AcSensor/2/Voltage - -AcSensor/3/Current - -AcSensor/3/Energy - -AcSensor/3/Location - -AcSensor/3/Phase - -AcSensor/3/Power - -AcSensor/3/Voltage - -AcSensor/4/Current - -AcSensor/4/Energy - -AcSensor/4/Location - -AcSensor/4/Phase - -AcSensor/4/Power - -AcSensor/4/Voltage - -AcSensor/5/Current - -AcSensor/5/Energy - -AcSensor/5/Location - -AcSensor/5/Phase - -AcSensor/5/Power - -AcSensor/5/Voltage - -AcSensor/6/Current - -AcSensor/6/Energy - -AcSensor/6/Location - -AcSensor/6/Phase - -AcSensor/6/Power - -AcSensor/6/Voltage - -AcSensor/7/Current - -AcSensor/7/Energy - -AcSensor/7/Location - -AcSensor/7/Phase - -AcSensor/7/Power - -AcSensor/7/Voltage - -AcSensor/8/Current - -AcSensor/8/Energy - -AcSensor/8/Location - -AcSensor/8/Phase - -AcSensor/8/Power - -AcSensor/8/Voltage - -AcSensor/Count - -Alarms/GridLost 0 -Alarms/HighDcCurrent 0 -Alarms/HighDcVoltage 0 -Alarms/HighTemperature 0 -Alarms/L1/HighTemperature 0 -Alarms/L1/LowBattery 0 -Alarms/L1/Overload 0 -Alarms/L1/Ripple 0 -Alarms/L2/HighTemperature 0 -Alarms/L2/LowBattery 0 -Alarms/L2/Overload 0 -Alarms/L2/Ripple 0 -Alarms/L3/HighTemperature 0 -Alarms/L3/LowBattery 0 -Alarms/L3/Overload 0 -Alarms/L3/Ripple 0 -Alarms/LowBattery 0 -Alarms/Overload 0 -Alarms/PhaseRotation 0 -Alarms/Ripple 0 -Alarms/TemperatureSensor 0 -Alarms/VoltageSensor 0 -BatteryOperationalLimits/BatteryLowVoltage 46.4 -BatteryOperationalLimits/MaxChargeCurrent 80 -BatteryOperationalLimits/MaxChargeVoltage 55.2 -BatteryOperationalLimits/MaxDischargeCurrent 120 -BatterySense/Temperature - -BatterySense/Voltage - -Bms/AllowToCharge 1 -Bms/AllowToChargeRate 0 -Bms/AllowToDischarge 1 -Bms/BmsExpected 0 -Bms/BmsType 0 -Bms/Error 0 -Bms/PreAlarm - -Connected 1 -CustomName -Dc/0/Current -9.2 -Dc/0/MaxChargeCurrent 70 -Dc/0/Power -447 -Dc/0/Temperature - -Dc/0/Voltage 52.92 -DeviceInstance 275 -Devices/0/Assistants 0 -Devices/0/ExtendStatus/ChargeDisabledDueToLowTemp 0 -Devices/0/ExtendStatus/ChargeIsDisabled 0 -Devices/0/ExtendStatus/GridRelayReport/Code - -Devices/0/ExtendStatus/GridRelayReport/Count - -Devices/0/ExtendStatus/GridRelayReport/Reset 0 -Devices/0/ExtendStatus/HighDcCurrent 0 -Devices/0/ExtendStatus/HighDcVoltage 0 -Devices/0/ExtendStatus/IgnoreAcIn1 0 -Devices/0/ExtendStatus/MainsPllLocked 1 -Devices/0/ExtendStatus/PcvPotmeterOnZero 0 -Devices/0/ExtendStatus/PowerPackPreOverload 0 -Devices/0/ExtendStatus/SocTooLowToInvert 0 -Devices/0/ExtendStatus/SustainMode 0 -Devices/0/ExtendStatus/SwitchoverInfo/Connecting 0 -Devices/0/ExtendStatus/SwitchoverInfo/Delay 0 -Devices/0/ExtendStatus/SwitchoverInfo/ErrorFlags 0 -Devices/0/ExtendStatus/TemperatureHighForceBypass 0 -Devices/0/ExtendStatus/VeBusNetworkQualityCounter 0 -Devices/0/ExtendStatus/WaitingForRelayTest 0 -Devices/0/InterfaceProtectionLog/0/ErrorFlags - -Devices/0/InterfaceProtectionLog/0/Time - -Devices/0/InterfaceProtectionLog/1/ErrorFlags - -Devices/0/InterfaceProtectionLog/1/Time - -Devices/0/InterfaceProtectionLog/2/ErrorFlags - -Devices/0/InterfaceProtectionLog/2/Time - -Devices/0/InterfaceProtectionLog/3/ErrorFlags - -Devices/0/InterfaceProtectionLog/3/Time - -Devices/0/InterfaceProtectionLog/4/ErrorFlags - -Devices/0/InterfaceProtectionLog/4/Time - -Devices/0/SerialNumber HQ00000AA01 -Devices/0/Version 2623497 -Devices/Bms/Version - -Devices/Dmc/Version - -Devices/NumberOfMultis 1 -Energy/AcIn1ToAcOut 0.0364089 -Energy/AcIn1ToInverter 2.69426 -Energy/AcIn2ToAcOut 0 -Energy/AcIn2ToInverter 0 -Energy/AcOutToAcIn1 0 -Energy/AcOutToAcIn2 0 -Energy/InverterToAcIn1 0.8192 -Energy/InverterToAcIn2 0 -Energy/InverterToAcOut 0.0364089 -Energy/OutToInverter 0.0182044 -ExtraBatteryCurrent 0 -FirmwareFeatures/BolFrame 1 -FirmwareFeatures/BolUBatAndTBatSense 1 -FirmwareFeatures/CommandWriteViaId 1 -FirmwareFeatures/IBatSOCBroadcast 1 -FirmwareFeatures/NewPanelFrame 1 -FirmwareFeatures/SetChargeState 1 -FirmwareSubVersion 0 -FirmwareVersion 1175 -Hub/ChargeVoltage 55.2 -Hub4/AssistantId 5 -Hub4/DisableCharge 0 -Hub4/DisableFeedIn 0 -Hub4/DoNotFeedInOvervoltage 1 -Hub4/FixSolarOffsetTo100mV 1 -Hub4/L1/AcPowerSetpoint -409 -Hub4/L1/CurrentLimitedDueToHighTemp 0 -Hub4/L1/FrequencyVariationOccurred 0 -Hub4/L1/MaxFeedInPower 32766 -Hub4/L1/OffsetAddedToVoltageSetpoint 0 -Hub4/Sustain 0 -Hub4/TargetPowerIsMaxFeedIn 0 -Interfaces/Mk2/Connection /dev/ttyS3 -Interfaces/Mk2/ProductId 4464 -Interfaces/Mk2/ProductName MK3 -Interfaces/Mk2/Status/BusFreeMode 1 -Interfaces/Mk2/Tunnel - -Interfaces/Mk2/Version 1170212 -Leds/Absorption 0 -Leds/Bulk 1 -Leds/Float 0 -Leds/Inverter 1 -Leds/LowBattery 0 -Leds/Mains 2 -Leds/Overload 0 -Leds/Temperature 0 -Mgmt/Connection VE.Bus -Mgmt/ProcessName mk2-dbus -Mgmt/ProcessVersion 3.39 -Mode 3 -ModeIsAdjustable 1 -ProductId 9763 -ProductName MultiPlus-II 48/5000/70-50 -PvInverter/Disable 0 -Quirks 0 -RedetectSystem 0 -Settings/Alarm/System/GridLost 1 -Settings/SystemSetup/AcInput1 1 -Settings/SystemSetup/AcInput2 0 -ShortIds 1 -Soc 74 -State 3 -SystemReset - -VebusChargeState 1 -VebusError 0 -VebusMainState 9