question

isimobile avatar image
isimobile asked

PropertiesChanged Dbus

I have the following code below, the callback track(conn, value) should be called when the "/Ac/Consumption/L1/Power" value changes ie property changes, bit it does not get called, anybody that can tell me why?


#!/usr/bin/python -u
    
from functools import partial
import dbus
from gi.repository import GLib
from dbus.mainloop.glib import DBusGMainLoop
    
def track(conn, value):
    if value["Value"] > 0:
        print("Yes")
    
def main():
    DBusGMainLoop(set_as_default=True)
    conn = dbus.SystemBus()
    
    conn.add_signal_receiver(partial(track, conn),
            dbus_interface='com.victronenergy.BusItem',
            signal_name='PropertiesChanged',
            path="/Ac/Consumption/L1/Power",
            bus_name="com.victronenergy.system")
    
    GLib.MainLoop().run()
    
if __name__ == "__main__":
    main()

I am running this on VenusOS 2.80~41-large-25

I am debugging the code from VS Code inside my MAC connected to my Cerbo GX device via SSH

Venus GX - VGX
2 |3000

Up to 8 attachments (including images) can be used with a maximum of 190.8 MiB each and 286.6 MiB total.

5 Answers
mvader (Victron Energy) avatar image
mvader (Victron Energy) answered ·

Hi, we added something new in v2.80: ItemsChanged. Its more cpu efficient than PropertiesChanged. And in many places, not all, PropertiesChanged was replaced with ItemsChanged.

See these links:

https://github.com/victronenergy/dbus-mqtt/commits/master

https://github.com/victronenergy/venus/wiki/dbus-api/_compare/33231957e502766b85424cc8e07a382d5b1bf499...43c68d319d845dfee1339a037a62e200d3e80391

2 |3000

Up to 8 attachments (including images) can be used with a maximum of 190.8 MiB each and 286.6 MiB total.

isimobile avatar image
isimobile answered ·

Thanks this helped. Got it working.

2 |3000

Up to 8 attachments (including images) can be used with a maximum of 190.8 MiB each and 286.6 MiB total.

schollex avatar image
schollex answered ·

I got this code working

#!/usr/bin/python -u

from functools import partial
import dbus
from gi.repository import GLib
from dbus.mainloop.glib import DBusGMainLoop

def track(conn, dict):
    print("######################")
    for k, v in dict.items():
      print(k, v['Value'])

def main():
    DBusGMainLoop(set_as_default=True)
    conn = dbus.SystemBus()

    conn.add_signal_receiver(partial(track, conn),
            dbus_interface='com.victronenergy.BusItem',
            signal_name='ItemsChanged',
            #path="/Ac/Grid/L1/Power",
            #path="/Ac/ConsumptionOnInput/L1/Power",
            path="/",
            bus_name="com.victronenergy.system")

    GLib.MainLoop().run()

if __name__ == "__main__":
    main()

But this does not work if you restrict the path="/" to anything else, e.g. path="/Ac/Grid/L1/Power". Then, you don't get any updates.

Can anybody explain this?

2 |3000

Up to 8 attachments (including images) can be used with a maximum of 190.8 MiB each and 286.6 MiB total.

schollex avatar image
schollex answered ·

I got it - it's by Victron's design that path is always "/" on the new-style signal_name='ItemsChanged'

On the old-style signal_name='PropertiesChanged' you can filter on path.

This code reports all, the old and new-style changes:

import dbus
from gi.repository import GLib
from dbus.mainloop.glib import DBusGMainLoop

def printdict(pre, value):
  if type(value) is dbus.Dictionary or type(value) is dict:
    for k, v in value.items():
      printdict('%s>%s' %(pre, k), v)
  else:
    print(pre, value)

def signal_handler(*args, **kwargs):
    print('kwargs: %s' %kwargs)
    printdict('', dict(enumerate(args)))
    print('---end----')

DBusGMainLoop(set_as_default=True)
bus = dbus.SystemBus()
#register your signal callback
bus.add_signal_receiver(signal_handler,
                        dbus_interface="com.victronenergy.BusItem",
                        interface_keyword='interface',
                        member_keyword='member',
                        path_keyword='path',
                        #message_keyword='msg',
)


loop = GLib.MainLoop()
loop.run()
 
2 |3000

Up to 8 attachments (including images) can be used with a maximum of 190.8 MiB each and 286.6 MiB total.

schollex avatar image
schollex answered ·

I had a look at /opt/victronenergy/dbus-recorder/.
This gives all the answers to the above topic. Then I took dbusrecorder.py and modified it to my needs:

  • cut off the unwanted file-recording-stuff
  • initally get all variables printed
  • and then get all changes of these variables
  • all is always kept up-to-date in the global values

This is the code of my new dbusdump.py

#!/usr/bin/python3 -u

# Python imports
from dbus.mainloop.glib import DBusGMainLoop
import dbus
import dbus.service
from gi.repository.GLib import timeout_add, MainLoop
from os import environ, path
from time import time
import sys
import signal
import platform
import getopt
import errno
import logging
from functools import partial

logger = logging.getLogger(__name__)

sys.path.insert(1, path.join(path.dirname(__file__), '/opt/victronenergy/dbus-recorder'))
from dbusdevice import DbusDevice

class Timer(object):
        def __init__(self, duration):
                self._duration = duration
                self._tick = 0

        def __call__(self):
                if self._tick >= self._duration:
                        logger.info("finished dumping with duration %ss" % self._duration)
                        sys.exit()
                        return False

                self._tick += 1
                return True

def handlerEvents(zeroTime, dbusName, dbusObjectPath, changes):
        global values
        values[dbusObjectPath] = {'Value': changes['Value'],  'Valid': bool(changes['Value'] != dbus.Array([])), 'Text': changes['Text']}
        logger.info('handlerEvents after %0.3fs - path: %s, %s: %s' % (time()-zeroTime, dbusName, dbusObjectPath, values[dbusObjectPath]))
        logger.debug('allValues: %s' % values)

## Handles the system (Linux / Windows) signals such as SIGTERM.
#
# Stops the logscript with an exit-code.
# @param signum the signal-number.
# @param stack the call-stack.
def handlerSignals(signum, stack):
        logger.warning('handlerSignals received: %d' % signum)
        exitCode = 0
        if signum == signal.SIGHUP:
                exitCode = 1
        sys.exit(exitCode)

def dump(device, duration):
        logger.info("Dumping %s" % device)
        dbusName = device.getBusName()
        global values
        values = device.getValues()
        logger.info('allValues: %s' % values)
        if duration:
                timer = Timer(duration)
                timer()
                timeout_add(1000, timer)

## The main function.
def run(service, duration):
        DBusGMainLoop(set_as_default=True)

        # setup signal handling.
        signal.signal(signal.SIGHUP, handlerSignals) # 1: Hangup detected
        signal.signal(signal.SIGINT, handlerSignals) # 2: Ctrl-C
        signal.signal(signal.SIGUSR1, handlerSignals) # 10: kill -USR1 <logscript-pid>
        signal.signal(signal.SIGTERM, handlerSignals) # 15: Terminate

        # get on the bus
        bus = dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in environ else dbus.SystemBus()

        # get named devices on the bus
        device = None
        for name in bus.list_names():
                if name.startswith(service):
                        logger.info("Introspecting: %s" % name)
                        device = DbusDevice(bus, name, partial(handlerEvents, int(round(time()))))
                        break
        if device is None:
                print("No dbus-service with name %s found!" % service)
                sys.exit()

        dump(device, duration)
        MainLoop().run()

def usage():
        print("Usage: ./dbusdump [OPTION]")
        print("-h, --help\tdisplay this help and exit")
        print("-d\t\tset logging level to debug (standard info)")
        print("--dump\tdump specified dbus-service")
        print("--duration\ttime duration dumping in seconds (0 is infinite)")

def main(argv):
        # Default logging level
        logging.basicConfig(level=logging.INFO)

        service = None
        duration = 0

        try:
                opts, args = getopt.getopt(argv, "vhd", ["help", "dump=", "duration="])
        except getopt.GetoptError:
                usage()
                sys.exit(errno.EINVAL)
        for opt, arg in opts:
                if opt == '-h' or opt == '--help':
                        usage()
                        sys.exit()
                elif opt == '-d':
                        logger.setLevel(logging.DEBUG)
                elif opt == '--dump':
                        if arg:
                                service = arg
                        else:
                                usage()
                                sys.exit()
                elif opt == '--duration':
                        try:
                                duration = int(arg)
                        except:
                                print("duration is not a valid number")
                                sys.exit()

        if service is None:
                usage()
                sys.exit()

        run(service, duration)

main(sys.argv[1:])

This code recognizes changes signaled via old-style PropertiesChanged as well an new-style ItemsChanged. I don't know which styles are used in current implementations. So it should be good to consider both :-)

2 |3000

Up to 8 attachments (including images) can be used with a maximum of 190.8 MiB each and 286.6 MiB total.