From c89572d05025a7a50b31feea298ea84a76982b99 Mon Sep 17 00:00:00 2001 From: kwindrem <58538395+kwindrem@users.noreply.github.com> Date: Sat, 13 Jul 2024 11:07:23 -0700 Subject: [PATCH] provide version-dependent velib_phthon for this and other packages --- FileSets/PatchSource/PageSettings.qml.patch | 4 +- PackageManager.py | 242 ++++--- changes | 4 + makeVelib_python | 238 +++++++ updatePackage | 87 ++- velib_python/latest/dbusmonitor.py | 587 ++++++++++++++++ velib_python/latest/oldestVersion | 1 + velib_python/latest/settingsdevice.py | 118 ++++ velib_python/latest/ve_utils.py | 276 ++++++++ velib_python/latest/vedbus.py | 646 ++++++++++++++++++ velib_python/v2.73/dbusmonitor.py | 535 +++++++++++++++ velib_python/v2.73/oldestVersion | 1 + velib_python/v2.73/settingsdevice.py | 115 ++++ velib_python/v2.73/ve_utils.py | 253 +++++++ velib_python/v2.73/vedbus.py | 501 ++++++++++++++ velib_python/v2.94/dbusmonitor.py | 592 ++++++++++++++++ velib_python/v2.94/oldestVersion | 1 + velib_python/v2.94/settingsdevice.py | 118 ++++ velib_python/v2.94/ve_utils.py | 265 +++++++ velib_python/v2.94/vedbus.py | 599 ++++++++++++++++ velib_python/v3.40~37/dbusmonitor.py | 554 +++++++++++++++ velib_python/v3.40~37/oldestVersion | 1 + velib_python/v3.40~37/settingsdevice.py | 118 ++++ velib_python/v3.40~37/ve_utils.py | 262 +++++++ velib_python/v3.40~37/vedbus.py | 611 +++++++++++++++++ velib_python/v3.40~38/dbusmonitor.py | 587 ++++++++++++++++ velib_python/v3.40~38/oldestVersion | 1 + velib_python/v3.40~38/settingsdevice.py | 118 ++++ velib_python/v3.40~38/ve_utils.py | 276 ++++++++ velib_python/v3.40~38/vedbus.py | 642 +++++++++++++++++ .../velib_python/latest/dbusmonitor.py | 587 ++++++++++++++++ .../velib_python/latest/oldestVersion | 1 + .../velib_python/latest/settingsdevice.py | 118 ++++ velib_python/velib_python/latest/ve_utils.py | 276 ++++++++ velib_python/velib_python/latest/vedbus.py | 646 ++++++++++++++++++ .../velib_python/v2.73/dbusmonitor.py | 535 +++++++++++++++ velib_python/velib_python/v2.73/oldestVersion | 1 + .../velib_python/v2.73/settingsdevice.py | 115 ++++ velib_python/velib_python/v2.73/ve_utils.py | 253 +++++++ velib_python/velib_python/v2.73/vedbus.py | 501 ++++++++++++++ .../velib_python/v2.94/dbusmonitor.py | 592 ++++++++++++++++ velib_python/velib_python/v2.94/oldestVersion | 1 + .../velib_python/v2.94/settingsdevice.py | 118 ++++ velib_python/velib_python/v2.94/ve_utils.py | 265 +++++++ velib_python/velib_python/v2.94/vedbus.py | 599 ++++++++++++++++ .../velib_python/v3.40~37/dbusmonitor.py | 554 +++++++++++++++ .../velib_python/v3.40~37/oldestVersion | 1 + .../velib_python/v3.40~37/settingsdevice.py | 118 ++++ .../velib_python/v3.40~37/ve_utils.py | 262 +++++++ velib_python/velib_python/v3.40~37/vedbus.py | 611 +++++++++++++++++ venus-data-UninstallPackages.tgz | Bin 1141 -> 0 bytes venus-data.tgz | Bin 190649 -> 0 bytes version | 2 +- 53 files changed, 14383 insertions(+), 126 deletions(-) create mode 100755 makeVelib_python create mode 100644 velib_python/latest/dbusmonitor.py create mode 100644 velib_python/latest/oldestVersion create mode 100644 velib_python/latest/settingsdevice.py create mode 100644 velib_python/latest/ve_utils.py create mode 100644 velib_python/latest/vedbus.py create mode 100644 velib_python/v2.73/dbusmonitor.py create mode 100644 velib_python/v2.73/oldestVersion create mode 100644 velib_python/v2.73/settingsdevice.py create mode 100644 velib_python/v2.73/ve_utils.py create mode 100644 velib_python/v2.73/vedbus.py create mode 100644 velib_python/v2.94/dbusmonitor.py create mode 100644 velib_python/v2.94/oldestVersion create mode 100644 velib_python/v2.94/settingsdevice.py create mode 100644 velib_python/v2.94/ve_utils.py create mode 100644 velib_python/v2.94/vedbus.py create mode 100644 velib_python/v3.40~37/dbusmonitor.py create mode 100644 velib_python/v3.40~37/oldestVersion create mode 100644 velib_python/v3.40~37/settingsdevice.py create mode 100644 velib_python/v3.40~37/ve_utils.py create mode 100644 velib_python/v3.40~37/vedbus.py create mode 100644 velib_python/v3.40~38/dbusmonitor.py create mode 100644 velib_python/v3.40~38/oldestVersion create mode 100644 velib_python/v3.40~38/settingsdevice.py create mode 100644 velib_python/v3.40~38/ve_utils.py create mode 100644 velib_python/v3.40~38/vedbus.py create mode 100644 velib_python/velib_python/latest/dbusmonitor.py create mode 100644 velib_python/velib_python/latest/oldestVersion create mode 100644 velib_python/velib_python/latest/settingsdevice.py create mode 100644 velib_python/velib_python/latest/ve_utils.py create mode 100644 velib_python/velib_python/latest/vedbus.py create mode 100644 velib_python/velib_python/v2.73/dbusmonitor.py create mode 100644 velib_python/velib_python/v2.73/oldestVersion create mode 100644 velib_python/velib_python/v2.73/settingsdevice.py create mode 100644 velib_python/velib_python/v2.73/ve_utils.py create mode 100644 velib_python/velib_python/v2.73/vedbus.py create mode 100644 velib_python/velib_python/v2.94/dbusmonitor.py create mode 100644 velib_python/velib_python/v2.94/oldestVersion create mode 100644 velib_python/velib_python/v2.94/settingsdevice.py create mode 100644 velib_python/velib_python/v2.94/ve_utils.py create mode 100644 velib_python/velib_python/v2.94/vedbus.py create mode 100644 velib_python/velib_python/v3.40~37/dbusmonitor.py create mode 100644 velib_python/velib_python/v3.40~37/oldestVersion create mode 100644 velib_python/velib_python/v3.40~37/settingsdevice.py create mode 100644 velib_python/velib_python/v3.40~37/ve_utils.py create mode 100644 velib_python/velib_python/v3.40~37/vedbus.py delete mode 100644 venus-data-UninstallPackages.tgz delete mode 100644 venus-data.tgz diff --git a/FileSets/PatchSource/PageSettings.qml.patch b/FileSets/PatchSource/PageSettings.qml.patch index d62a8c8..80ec615 100644 --- a/FileSets/PatchSource/PageSettings.qml.patch +++ b/FileSets/PatchSource/PageSettings.qml.patch @@ -1,5 +1,5 @@ ---- /Users/Kevin/GitHub/SetupHelper.copy/FileSets/PatchSource/PageSettings.qml.orig 2024-04-13 11:31:17 -+++ /Users/Kevin/GitHub/SetupHelper.copy/FileSets/PatchSource/PageSettings.qml 2024-04-13 11:31:17 +--- /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 diff --git a/PackageManager.py b/PackageManager.py index 9ceea74..03d30f8 100755 --- a/PackageManager.py +++ b/PackageManager.py @@ -383,12 +383,6 @@ import Queue as queue import gobject as GLib -# add the path to our own packages for import -# use an established Victron service to maintain compatiblity -sys.path.insert(1, os.path.join('/opt/victronenergy/dbus-systemcalc-py', 'ext', 'velib_python')) -from vedbus import VeDbusService -from settingsdevice import SettingsDevice - global DownloadGitHub global InstallPackages global AddRemove @@ -402,6 +396,129 @@ global WaitForGitHubVersions # initialized in main, set in UpdateGitHubVersion used in mainLoop global InitializePackageManager # initialized/used in main, set in PushAction, MediaScan run, used in mainloop + +# convert a version string to an integer to make comparisions easier +# the Victron format for version numbers is: vX.Y~Z-large-W +# the ~Z portion indicates a pre-release version so a version without it is "newer" than a version with it +# the -W portion has been abandoned but was like the ~Z for large builds and is IGNORED !!!! +# large builds now have the same version number as the "normal" build +# +# the version string passed to this function allows for quite a bit of flexibility +# any alpha characters are permitted prior to the first digit +# up to 3 version parts PLUS a prerelease part are permitted +# each with up to 4 digits each -- MORE THAN 4 digits is indeterminate +# that is: v0.0.0d0 up to v9999.9999.9999b9999 and then v9999.9999.9999 as the highest priority +# any non-numeric character can be used to separate main versions +# special significance is assigned to single caracter separators between the numeric strings +# b or ~ indicates a beta release +# a indicates an alpha release +# d indicates an development release +# these offset the pre-release number so that b/~ has higher numeric value than any a +# and a has higher value than d separator +# +# a blank version or one without at least one number part is considered invalid +# alpha and beta seperators require at least two number parts +# if only one number part is found the prerelease seperator is IGNORED +# +# returns the version number or 0 if string does not parse into needed sections + +def VersionToNumber (version): + version = version.replace ("large","L") + numberParts = re.split ('\D+', version) + otherParts = re.split ('\d+', version) + # discard blank elements + # this can happen if the version string starts with alpha characters (like "v") + # of if there are no numeric digits in the version string + try: + while numberParts [0] == "": + numberParts.pop(0) + except: + pass + + numberPartsLength = len (numberParts) + + if numberPartsLength == 0: + return 0 + versionNumber = 0 + releaseType='release' + if numberPartsLength >= 2: + if 'b' in otherParts or '~' in otherParts: + releaseType = 'beta' + versionNumber += 60000 + elif 'a' in otherParts: + releaseType = 'alpha' + versionNumber += 30000 + elif 'd' in otherParts: + releaseType = 'develop' + + # if release all parts contribute to the main version number + # and offset is greater than all prerelease versions + if releaseType == 'release': + versionNumber += 90000 + # if pre-release, last part will be the pre release part + # and others part will be part the main version number + else: + numberPartsLength -= 1 + versionNumber += int (numberParts [numberPartsLength]) + + # include core version number + versionNumber += int (numberParts [0]) * 10000000000000 + if numberPartsLength >= 2: + versionNumber += int (numberParts [1]) * 1000000000 + if numberPartsLength >= 3: + versionNumber += int (numberParts [2]) * 100000 + + return versionNumber + + +# get venus version +versionFile = "/opt/victronenergy/version" +try: + file = open (versionFile, 'r') +except: + VenusVersion = "" + VenusVersionNumber = 0 +else: + VenusVersion = file.readline().strip() + VenusVersionNumber = VersionToNumber (VenusVersion) + file.close() + +# add the path to our own packages for import +# use an established Victron service to maintain compatiblity +setupHelperVeLibPath = "/data/SetupHelper/velib_python" +veLibPath = "" +if os.path.exists ( setupHelperVeLibPath ): + for libVersion in os.listdir ( setupHelperVeLibPath ): + # use 'latest' for newest versions even if not specifically checked against this verison when created + if libVersion == "latest": + newestVersionNumber = VersionToNumber ( "v9999.9999.9999" ) + else: + newestVersionNumber = VersionToNumber ( libVersion ) + oldestVersionPath = os.path.join (setupHelperVeLibPath, libVersion, "oldestVersion" ) + if os.path.exists ( oldestVersionPath ): + try: + fd = open (oldestVersionPath, 'r') + oldestVersionNumber = VersionToNumber ( fd.readline().strip () ) + fd.close() + except: + oldestVersionNumber = 0 + else: + oldestVersionNumber = 0 + if VenusVersionNumber >= oldestVersionNumber and VenusVersionNumber <= newestVersionNumber: + veLibPath = os.path.join (setupHelperVeLibPath, libVersion) + break + +# no SetupHelper velib - use one in systemcalc +if veLibPath == "": + veLibPath = os.path.join('/opt/victronenergy/dbus-systemcalc-py', 'ext', 'velib_python') + +logging.warning ("using " + veLibPath + " for velib_python") +sys.path.insert(1, veLibPath) + +from vedbus import VeDbusService +from settingsdevice import SettingsDevice + + # PushAction # # some actions are pushed to one of three queues: @@ -563,82 +680,6 @@ def PushAction (command=None, source=None): # end PushAction -# convert a version string to an integer to make comparisions easier -# the Victron format for version numbers is: vX.Y~Z-large-W -# the ~Z portion indicates a pre-release version so a version without it is "newer" than a version with it -# the -W portion has been abandoned but was like the ~Z for large builds and is IGNORED !!!! -# large builds now have the same version number as the "normal" build -# -# the version string passed to this function allows for quite a bit of flexibility -# any alpha characters are permitted prior to the first digit -# up to 3 version parts PLUS a prerelease part are permitted -# each with up to 4 digits each -- MORE THAN 4 digits is indeterminate -# that is: v0.0.0d0 up to v9999.9999.9999b9999 and then v9999.9999.9999 as the highest priority -# any non-numeric character can be used to separate main versions -# special significance is assigned to single caracter separators between the numeric strings -# b or ~ indicates a beta release -# a indicates an alpha release -# d indicates an development release -# these offset the pre-release number so that b/~ has higher numeric value than any a -# and a has higher value than d separator -# -# a blank version or one without at least one number part is considered invalid -# alpha and beta seperators require at least two number parts -# if only one number part is found the prerelease seperator is IGNORED -# -# returns the version number or 0 if string does not parse into needed sections - -def VersionToNumber (version): - version = version.replace ("large","L") - numberParts = re.split ('\D+', version) - otherParts = re.split ('\d+', version) - # discard blank elements - # this can happen if the version string starts with alpha characters (like "v") - # of if there are no numeric digits in the version string - try: - while numberParts [0] == "": - numberParts.pop(0) - except: - pass - - numberPartsLength = len (numberParts) - - if numberPartsLength == 0: - return 0 - versionNumber = 0 - releaseType='release' - if numberPartsLength >= 2: - if 'b' in otherParts or '~' in otherParts: - releaseType = 'beta' - versionNumber += 60000 - elif 'a' in otherParts: - releaseType = 'alpha' - versionNumber += 30000 - elif 'd' in otherParts: - releaseType = 'develop' - - - - # if release all parts contribute to the main version number - # and offset is greater than all prerelease versions - if releaseType == 'release': - versionNumber += 90000 - # if pre-release, last part will be the pre release part - # and others part will be part the main version number - else: - numberPartsLength -= 1 - versionNumber += int (numberParts [numberPartsLength]) - - # include core version number - versionNumber += int (numberParts [0]) * 10000000000000 - if numberPartsLength >= 2: - versionNumber += int (numberParts [1]) * 1000000000 - if numberPartsLength >= 3: - versionNumber += int (numberParts [2]) * 100000 - - return versionNumber - - # LocatePackagePath # # attempt to locate a package directory @@ -1197,18 +1238,24 @@ def __init__(self): self.DbusSettings = SettingsDevice(bus=dbus.SystemBus(), supportedSettings=settingsList, timeout = 30, eventCallback=None ) - self.DbusService = VeDbusService ('com.victronenergy.packageManager', bus = dbus.SystemBus()) + # check firmware version and delay dbus service registration for v3.40~38 and beyond + global VenusVersionNumber + global VersionToNumber + versionThreshold = VersionToNumber ("v3.40~28") + if VenusVersionNumber >= versionThreshold: + self.DbusService = VeDbusService ('com.victronenergy.packageManager', bus = dbus.SystemBus(), register=False) + delayedRegistration = True + else: + self.DbusService = VeDbusService ('com.victronenergy.packageManager', bus = dbus.SystemBus()) + delayedRegistration = False + self.DbusService.add_mandatory_paths ( processname = 'PackageManager', processversion = 1.0, connection = 'none', deviceinstance = 0, productid = 1, productname = 'Package Manager', firmwareversion = 1, hardwareversion = 0, connected = 1) - self.DbusService.add_path ( '/PmStatus', "", writeable = True ) self.DbusService.add_path ( '/MediaUpdateStatus', "", writeable = True ) self.DbusService.add_path ( '/GuiEditStatus', "", writeable = True ) - global Platform - self.DbusService.add_path ( '/Platform', Platform ) - self.DbusService.add_path ( '/GuiEditAction', "", writeable = True, onchangecallback = self.handleGuiEditAction ) @@ -1238,6 +1285,13 @@ def __init__(self): self.DbusService.add_path ( '/BackupSettingsLocalFileExist', 0, writeable = True ) self.DbusService.add_path ( '/BackupProgress', 0, writeable = True ) + # do these last because the GUI uses them to check if PackageManager is running + self.DbusService.add_path ( '/PmStatus', "", writeable = True ) + global Platform + self.DbusService.add_path ( '/Platform', Platform ) + if delayedRegistration: + self.DbusService.register () + # RemoveDbusService # deletes the dbus service @@ -4005,20 +4059,6 @@ def main(): if PythonVersion < (3, 0): GLib.threads_init() - # get venus version - global VenusVersion - global VenusVersionNumber - global VersionToNumber - versionFile = "/opt/victronenergy/version" - try: - file = open (versionFile, 'r') - except: - VenusVersion = "" - else: - VenusVersion = file.readline().strip() - file.close() - VenusVersionNumber = VersionToNumber (VenusVersion) - # get platform global Platform platformFile = "/etc/venus/machine" diff --git a/changes b/changes index 3e8db7e..eb4d975 100644 --- a/changes +++ b/changes @@ -1,3 +1,7 @@ +v8.7: + updatePackage: always rebuild patch files + provide version-dependent velib_phthon for this and other packages + v8.6: fixed: persistent download pending message after a download fails diff --git a/makeVelib_python b/makeVelib_python new file mode 100755 index 0000000..71f8a95 --- /dev/null +++ b/makeVelib_python @@ -0,0 +1,238 @@ +#!/bin/bash + + + +# convert a version string to an integer to make comparisions easier +# +# Note: copied from VersionResources +# but also includes code to report duplcates not in the VersionResources version + +function versionStringToNumber () +{ + local version="$*" + local numberParts + local versionParts + local numberParts + local otherParts + local other + local number=0 + local type='release' + + # split incoming string into + # an array of numbers: major, minor, prerelease, etc + # and an array of other substrings + # the other array is searched for releasy type strings and the related offest added to the version number + + read -a numberParts <<< $(echo $version | tr -cs '0-9' ' ') + numberPartsLength=${#numberParts[@]} + if (( $numberPartsLength == 0 )); then + versionNumber=0 + versionStringToNumberStatus="$version: invalid, missing major version" + return 1 + fi + if (( $numberPartsLength >= 2 )); then + read -a otherParts <<< $(echo $version | tr -s '0-9' ' ') + for other in ${otherParts[@]}; do + case $other in + 'b' | '~') + type='beta' + (( number += 60000 )) + break ;; + 'a') + type='alpha' + (( number += 30000 )) + break ;; + 'd') + type='develop' + break ;; + esac + done + fi + + # if release all parts contribute to the main version number + # and offset is greater than all prerelease versions + if [ "$type" == "release" ] ; then + (( number += 90000 )) + # if pre-release, last part will be the pre release part + # and others part will be part the main version number + else + (( numberPartsLength-- )) + (( number += 10#${numberParts[$numberPartsLength]} )) + fi + # include core version number + (( number += 10#${numberParts[0]} * 10000000000000 )) + if (( numberPartsLength >= 2)); then + (( number += 10#${numberParts[1]} * 1000000000 )) + fi + if (( numberPartsLength >= 3)); then + (( number += 10#${numberParts[2]} * 100000 )) + fi + + versionNumber=$number + versionStringToNumberStatus="$version:$number $type" + return 0 +} + + +totalErrors=0 +totalWarnings=0 +packageErrors=0 +packageWarnings=0 + +outputtingProgress=false + + +function logMessage () +{ + if $outputtingProgress ; then + clearProgress + fi + echo "$*" + if [[ "$*" == "ERROR"* ]]; then + ((totalErrors++)) + ((packageErrors++)) + elif [[ "$*" == "WARNING"* ]]; then + ((totalWarnings++)) + ((packageWarnings++)) + fi +} + +function outputProgressTick () +{ + if ! $outputtingProgress ; then + echo -en "$beginProgressString" + fi + echo -en "$1" + outputtingProgress=true +} + +function clearProgress () +{ + # start a new line if outputting ticks + if $outputtingProgress; then + echo + # echo -ne "\r\033[2K" #### erase line + fi + outputtingProgress=false +} + +beginProgressString="" + +function beginProgress () +{ + # erase the line but stay on it + if $outputtingProgress ; then + clearProgress + fi + if [ ! -z "$1" ]; then + beginProgressString="$1 " + echo -en "$beginProgressString" + + outputtingProgress=true + fi +} + + + +#### script code begins here + +# attempt to locate SharedUtilities based on the location of this script +# (it is assumed to be in the SetupHelper directory) +# also sets the package root directory based on this also +# and also the stock files base directory +# +# if these are not correct, edit the lines below to set the appropriate values + +scriptDir="$( cd $(dirname "$0") >/dev/null 2>&1 ; /bin/pwd -P )" +packageRoot="$( dirname $scriptDir )" +stockFiles="$packageRoot/StockVenusOsFiles" +pythonLibDir="opt/victronenergy/dbus-systemcalc-py/ext/velib_python" +veLibFiles=( vedbus.py dbusmonitor.py settingsdevice.py ve_utils.py ) + +#### set these as appropriate to your system if the values set above are not correct +#### packageRoot=FILL_THIS_IN_AND_UNCOMMENT_LINE +#### stockFiles=FILL_THIS_IN_AND_UNCOMMENT_LINE + +if [ ! -e "$packageRoot" ]; then + echo "unable to locate package root - can't continue" + exit +elif [ ! -e "$stockFiles" ]; then + echo "unable to locate stock files - can't continue" + exit +fi + + +# make the version list from the directories in stock files +# version lists are sorted so the most recent version is first +tempList=() +stockVersionList=($(ls -d "$stockFiles"/v[0-9]* 2> /dev/null)) +for entry in ${stockVersionList[@]} ; do + version=$(basename $entry) + versionFile="$stockFiles/$version/opt/victronenergy/version" + if [ -f "$versionFile" ]; then + realVersion=$(cat "$versionFile" | head -n 1) + else + logMessage "ERROR version file missing from stock files $version - can't continue" + exit + fi + + if [ $version != $realVersion ]; then + logMessage "ERROR $version name does not mactch Venus $realVersion - can't continue" + exit + fi + if versionStringToNumber $version ; then + tempList+=("$version:$versionNumber") + else + logMessage "ERROR invalid version $versionStringToNumberStatus - not added to list" + fi +done +stockVersionList=( $(echo ${tempList[@]} | tr ' ' '\n' | sort -t ':' -r -n -k 2 | uniq ) ) +stockVersionListLength=${#stockVersionList[@]} + +if (( stockVersionListLength < 2 )); then + logMessage "fewer than 2 versions - nothing to compare" + exit +fi + +if [ -e "$scriptDir/velib_python" ]; then + rm -rf "$scriptDir/velib_python" +fi +mkdir -p "$scriptDir/velib_python" + +for (( i1 = 0; i1 < $stockVersionListLength; i1++ )); do + newVersion=false + IFS=':' read version versionNumber <<< "${stockVersionList[$i1]}" + + if (( i1 == 0 )); then + newVersion=true + else + for file in ${veLibFiles[@]} ; do + file1="$stockFiles/$version/$pythonLibDir/$file" + file2="$stockFiles/$previousVersion/$pythonLibDir/$file" + if ! cmp -s "$file1" "$file2" > /dev/null ; then + logMessage " $file $previousVersion $version differ" + newVersion=true + fi + done + fi + + if $newVersion ; then + if (( i1 == 0 ));then + velibDir="$scriptDir/velib_python/latest" + prevVelibDir="$scriptDir/velib_python/latest" + else + velibDir="$scriptDir/velib_python/$version" + fi + mkdir "$velibDir" + logMessage "new velib_python version $version" + for file in ${veLibFiles[@]} ; do + file1="$stockFiles/$version/$pythonLibDir/$file" + file2="$velibDir/$file" + cp -f "$file1" "$file2" + done + newVersion=false + previousVersion=$version + prevVelibDir="$velibDir" + fi + echo $version > "$prevVelibDir/oldestVersion" +done diff --git a/updatePackage b/updatePackage index e3cfaac..b934c84 100755 --- a/updatePackage +++ b/updatePackage @@ -425,13 +425,19 @@ if [ "$globalEndAction" == "restore" ]; then sourceFiles="$sourceDirectory/FileSets" backupDirectory="$packageRoot/$package.backup" backupFiles="$backupDirectory/FileSets" + sourceVeLib="$sourceDirectory/velib_python" + backupVeLib="$backupDirectory/velib_python" if [ ! -d "$backupDirectory" ]; then logMessage "WARNING $package: no backup found - package NOT restored" continue fi logMessage "WARNING $package: restored from backup" deleteNestedDirectories "$sourceFiles" + deleteNestedDirectories "$sourceVeLib" mv "$backupFiles" "$sourceFiles" + if [ -e "$backupVeLib" ]; then + mv -f "$backupVeLib" "$sourceVeLib" + fi deleteNestedDirectories $backupDirectory done exit @@ -473,6 +479,7 @@ for entry in ${stockVersionList[@]} ; do fi done stockVersionList=( $(echo ${tempList[@]} | tr ' ' '\n' | sort -t ':' -r -n -k 2 | uniq ) ) +stockVersionListLength=${#stockVersionList[@]} for package in $packageList; do packageErrors=0 @@ -485,6 +492,8 @@ for package in $packageList; do backupDirectory="$packageRoot/$package.backup" backupFiles="$backupDirectory/FileSets" versionIndependentFileSet="$workingFiles/VersionIndependent" + sourceVeLib="$sourceDirectory/velib_python" + workingVeLib="$workingDirectory/velib_python" if [ ! -d "$sourceDirectory" ] || [ ! -f "$sourceDirectory/version" ]; then logMessage "WARNING: $sourceDirectory - not a package directory - skipping" @@ -557,6 +566,9 @@ for package in $packageList; do if [ -d "$sourceFiles" ]; then cp -pR "$sourceFiles" "$workingFiles" fi + if [ -d "$sourceVeLib" ]; then + cp -pR "$sourceVeLib" "$workingVeLib" + fi fi # clean up flag files from a previous run @@ -576,6 +588,52 @@ for package in $packageList; do rm -f "$workingFiles"/*CHECK_ALT_ORIG rm -f "$workingFiles"/*/*CHECK_ALT_ORIG + # update velib_python + if [ -e "$workingVeLib" ]; then + beginProgress "updating velib_python" + pythonLibSoureDir="opt/victronenergy/dbus-systemcalc-py/ext/velib_python" + veLibFiles=( vedbus.py dbusmonitor.py settingsdevice.py ve_utils.py ) + rm -rf "$workingVeLib" + mkdir "$workingVeLib" + + for (( i1 = 0; i1 < $stockVersionListLength; i1++ )); do + newVersion=false + IFS=':' read version versionNumber <<< "${stockVersionList[$i1]}" + + if (( i1 == 0 )); then + newVersion=true + else + for file in ${veLibFiles[@]} ; do + file1="$stockFiles/$version/$pythonLibSoureDir/$file" + file2="$stockFiles/$previousVersion/$pythonLibSoureDir/$file" + if ! cmp -s "$file1" "$file2" > /dev/null ; then + newVersion=true + fi + done + fi + + if $newVersion ; then + outputProgressTick "." + if (( i1 == 0 ));then + velibDir="$workingVeLib/latest" + prevVelibDir="$workingVeLib/latest" + else + velibDir="$workingVeLib/$version" + fi + mkdir "$velibDir" + for file in ${veLibFiles[@]} ; do + file1="$stockFiles/$version/$pythonLibSoureDir/$file" + file2="$velibDir/$file" + cp -f "$file1" "$file2" + done + newVersion=false + previousVersion=$version + prevVelibDir="$velibDir" + fi + echo $version > "$prevVelibDir/oldestVersion" + done + fi + getFileLists "$workingFiles" # if any version-dependent files, create missing file sets or flag incompatible @@ -731,27 +789,7 @@ for package in $packageList; do continue fi - updatePatch=false - if [ -f "$patchFile" ]; then - origModTime=$( date -r "$patchOrig" '+%s' ) - resultModTime=$( date -r "$patchResult" '+%s' ) - patchModTime=$( date -r "$patchFile" '+%s' ) - if (( origModTime > patchModTime )); then - updatePatch=true - elif (( resultModTime > patchModTime )); then - updatePatch=true - elif (( optionsModTime > patchModTime )); then - updatePatch=true - else - updatePatch=false - fi - else - logMessage "$package: creating $( basename $patchFile )" - updatePatch=true - fi - if $updatePatch ; then - diff $patchOptions "$patchOrig" "$patchResult" > "$patchFile" - fi + diff $patchOptions "$patchOrig" "$patchResult" > "$patchFile" done fi @@ -794,7 +832,7 @@ for package in $packageList; do if $foundPatch; then replacementFile="$workingFiles/$version/$baseName" if [ -e "$replacement" ] && ! [ -L "$replacement" ] || [ -e "$replacement.USE_ORIGINAL" ]; then - logMessage "WARNING $package: removing $baseName from $version" + logMessage "WARNING $package: removing $baseName from $version file set" rm -f "$replacement"* fi else @@ -1258,6 +1296,11 @@ for package in $packageList; do if [ -d "$sourceFiles" ]; then mv "$sourceFiles" "$backupFiles" fi + sourceVeLib="$sourceDirectory/velib_python" + workingVeLib="$workingDirectory/velib_python" + if [ -d "$workingVeLib" ]; then + mv -f "$workingVeLib" "$sourceVeLib" + fi else logMessage "$package: $backupName unchanged" fi diff --git a/velib_python/latest/dbusmonitor.py b/velib_python/latest/dbusmonitor.py new file mode 100644 index 0000000..fd25700 --- /dev/null +++ b/velib_python/latest/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/latest/oldestVersion b/velib_python/latest/oldestVersion new file mode 100644 index 0000000..bdea676 --- /dev/null +++ b/velib_python/latest/oldestVersion @@ -0,0 +1 @@ +v3.40~39 diff --git a/velib_python/latest/settingsdevice.py b/velib_python/latest/settingsdevice.py new file mode 100644 index 0000000..a207e8b --- /dev/null +++ b/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/latest/ve_utils.py b/velib_python/latest/ve_utils.py new file mode 100644 index 0000000..f5a2f85 --- /dev/null +++ b/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/latest/vedbus.py b/velib_python/latest/vedbus.py new file mode 100644 index 0000000..cb95ba1 --- /dev/null +++ b/velib_python/latest/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/velib_python/v2.73/dbusmonitor.py b/velib_python/v2.73/dbusmonitor.py new file mode 100644 index 0000000..fc8785d --- /dev/null +++ b/velib_python/v2.73/dbusmonitor.py @@ -0,0 +1,535 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +## @package dbus_vrm +# This code takes care of the D-Bus interface (not all of below is implemented yet): +# - on startup it scans the dbus for services we know. For each known service found, it searches for +# objects/paths we know. Everything we find is stored in items{}, and an event is registered: if a +# value changes weĺl be notified and can pass that on to our owner. For example the vrmLogger. +# we know. +# - after startup, it continues to monitor the dbus: +# 1) when services are added we do the same check on that +# 2) when services are removed, we remove any items that we had that referred to that service +# 3) if an existing services adds paths we update ourselves as well: on init, we make a +# VeDbusItemImport for a non-, or not yet existing objectpaths as well1 +# +# Code is used by the vrmLogger, and also the pubsub code. Both are other modules in the dbus_vrm repo. + +from dbus.mainloop.glib import DBusGMainLoop +import gobject +from gobject import idle_add +import dbus +import dbus.service +import inspect +import logging +import argparse +import pprint +import traceback +import os +from collections import defaultdict +from functools import partial + +# our own packages +from vedbus import VeDbusItemExport, VeDbusItemImport +from ve_utils import exit_on_error, wrap_dbus_value, unwrap_dbus_value +notfound = object() # For lookups where None is a valid result + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) +class SystemBus(dbus.bus.BusConnection): + def __new__(cls): + return dbus.bus.BusConnection.__new__(cls, dbus.bus.BusConnection.TYPE_SYSTEM) + +class SessionBus(dbus.bus.BusConnection): + def __new__(cls): + return dbus.bus.BusConnection.__new__(cls, dbus.bus.BusConnection.TYPE_SESSION) + +class MonitoredValue(object): + def __init__(self, value, text, options): + super(MonitoredValue, self).__init__() + self.value = value + self.text = text + self.options = options + + # For legacy code, allow treating this as a tuple/list + def __iter__(self): + return iter((self.value, self.text, self.options)) + +class Service(object): + whentologoptions = ['configChange', 'onIntervalAlwaysAndOnEvent', + 'onIntervalOnlyWhenChanged', 'onIntervalAlways', 'never'] + def __init__(self, id, serviceName, deviceInstance): + super(Service, self).__init__() + self.id = id + self.name = serviceName + self.paths = {} + self._seen = set() + self.deviceInstance = deviceInstance + + self.configChange = [] + self.onIntervalAlwaysAndOnEvent = [] + self.onIntervalOnlyWhenChanged = [] + self.onIntervalAlways = [] + self.never = [] + + # For legacy code, attributes can still be accessed as if keys from a + # dictionary. + def __setitem__(self, key, value): + self.__dict__[key] = value + def __getitem__(self, key): + return self.__dict__[key] + + def set_seen(self, path): + self._seen.add(path) + + def seen(self, path): + return path in self._seen + + @property + def service_class(self): + return '.'.join(self.name.split('.')[:3]) + +class DbusMonitor(object): + ## Constructor + def __init__(self, dbusTree, valueChangedCallback=None, deviceAddedCallback=None, + deviceRemovedCallback=None, vebusDeviceInstance0=False): + # valueChangedCallback is the callback that we call when something has changed. + # def value_changed_on_dbus(dbusServiceName, dbusPath, options, changes, deviceInstance): + # in which changes is a tuple with GetText() and GetValue() + self.valueChangedCallback = valueChangedCallback + self.deviceAddedCallback = deviceAddedCallback + self.deviceRemovedCallback = deviceRemovedCallback + self.dbusTree = dbusTree + self.vebusDeviceInstance0 = vebusDeviceInstance0 + + # Lists all tracked services. Stores name, id, device instance, value per path, and whenToLog info + # indexed by service name (eg. com.victronenergy.settings). + self.servicesByName = {} + + # Same values as self.servicesByName, but indexed by service id (eg. :1.30) + self.servicesById = {} + + # Keep track of services by class to speed up calls to get_service_list + self.servicesByClass = defaultdict(list) + + # Keep track of any additional watches placed on items + self.serviceWatches = defaultdict(list) + + # For a PC, connect to the SessionBus + # For a CCGX, connect to the SystemBus + self.dbusConn = SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else SystemBus() + + # subscribe to NameOwnerChange for bus connect / disconnect events. + (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ \ + else dbus.SystemBus()).add_signal_receiver( + self.dbus_name_owner_changed, + signal_name='NameOwnerChanged') + + # Subscribe to PropertiesChanged for all services + self.dbusConn.add_signal_receiver(self.handler_value_changes, + dbus_interface='com.victronenergy.BusItem', + signal_name='PropertiesChanged', path_keyword='path', + sender_keyword='senderId') + + logger.info('===== Search on dbus for services that we will monitor starting... =====') + serviceNames = self.dbusConn.list_names() + for serviceName in serviceNames: + self.scan_dbus_service(serviceName) + + logger.info('===== Search on dbus for services that we will monitor finished =====') + + def dbus_name_owner_changed(self, name, oldowner, newowner): + if not name.startswith("com.victronenergy."): + return + + #decouple, and process in main loop + idle_add(exit_on_error, self._process_name_owner_changed, name, oldowner, newowner) + + def _process_name_owner_changed(self, name, oldowner, newowner): + if newowner != '': + # so we found some new service. Check if we can do something with it. + newdeviceadded = self.scan_dbus_service(name) + if newdeviceadded and self.deviceAddedCallback is not None: + self.deviceAddedCallback(name, self.get_device_instance(name)) + + elif name in self.servicesByName: + # it disappeared, we need to remove it. + logger.info("%s disappeared from the dbus. Removing it from our lists" % name) + service = self.servicesByName[name] + deviceInstance = service['deviceInstance'] + del self.servicesById[service.id] + del self.servicesByName[name] + for watch in self.serviceWatches[name]: + watch.remove() + del self.serviceWatches[name] + self.servicesByClass[service.service_class].remove(service) + if self.deviceRemovedCallback is not None: + self.deviceRemovedCallback(name, deviceInstance) + + def scan_dbus_service(self, serviceName): + try: + return self.scan_dbus_service_inner(serviceName) + except: + logger.error("Ignoring %s because of error while scanning:" % (serviceName)) + traceback.print_exc() + return False + + # Errors 'org.freedesktop.DBus.Error.ServiceUnknown' and + # 'org.freedesktop.DBus.Error.Disconnected' seem to happen when the service + # disappears while its being scanned. Which might happen, but is not really + # normal either, so letting them go into the logs. + + # Scans the given dbus service to see if it contains anything interesting for us. If it does, add + # it to our list of monitored D-Bus services. + def scan_dbus_service_inner(self, serviceName): + + # make it a normal string instead of dbus string + serviceName = str(serviceName) + + paths = self.dbusTree.get('.'.join(serviceName.split('.')[0:3]), None) + if paths is None: + logger.debug("Ignoring service %s, not in the tree" % serviceName) + return False + + logger.info("Found: %s, scanning and storing items" % serviceName) + serviceId = self.dbusConn.get_name_owner(serviceName) + + # we should never be notified to add a D-Bus service that we already have. If this assertion + # raises, check process_name_owner_changed, and D-Bus workings. + assert serviceName not in self.servicesByName + assert serviceId not in self.servicesById + + # for vebus.ttyO1, this is workaround, since VRM Portal expects the main vebus + # devices at instance 0. Not sure how to fix this yet. + if serviceName == 'com.victronenergy.vebus.ttyO1' and self.vebusDeviceInstance0: + di = 0 + elif serviceName == 'com.victronenergy.settings': + di = 0 + elif serviceName.startswith('com.victronenergy.vecan.'): + di = 0 + else: + try: + di = self.dbusConn.call_blocking(serviceName, + '/DeviceInstance', None, 'GetValue', '', []) + except dbus.exceptions.DBusException: + logger.info(" %s was skipped because it has no device instance" % serviceName) + return False # Skip it + else: + di = int(di) + + logger.info(" %s has device instance %s" % (serviceName, di)) + service = Service(serviceId, serviceName, di) + + # Let's try to fetch everything in one go + values = {} + texts = {} + try: + values.update(self.dbusConn.call_blocking(serviceName, '/', None, 'GetValue', '', [])) + texts.update(self.dbusConn.call_blocking(serviceName, '/', None, 'GetText', '', [])) + except: + pass + + for path, options in paths.iteritems(): + # path will be the D-Bus path: '/Ac/ActiveIn/L1/V' + # options will be a dictionary: {'code': 'V', 'whenToLog': 'onIntervalAlways'} + # check that the whenToLog setting is set to something we expect + assert options['whenToLog'] is None or options['whenToLog'] in Service.whentologoptions + + # Try to obtain the value we want from our bulk fetch. If we + # cannot find it there, do an individual query. + value = values.get(path[1:], notfound) + if value != notfound: + service.set_seen(path) + text = texts.get(path[1:], notfound) + if value is notfound or text is notfound: + try: + value = self.dbusConn.call_blocking(serviceName, path, None, 'GetValue', '', []) + service.set_seen(path) + text = self.dbusConn.call_blocking(serviceName, path, None, 'GetText', '', []) + except dbus.exceptions.DBusException as e: + if e.get_dbus_name() in ( + 'org.freedesktop.DBus.Error.ServiceUnknown', + 'org.freedesktop.DBus.Error.Disconnected'): + raise # This exception will be handled below + + # TODO org.freedesktop.DBus.Error.UnknownMethod really + # shouldn't happen but sometimes does. + logger.debug("%s %s does not exist (yet)" % (serviceName, path)) + value = None + text = None + + service.paths[path] = MonitoredValue(unwrap_dbus_value(value), unwrap_dbus_value(text), options) + + if options['whenToLog']: + service[options['whenToLog']].append(path) + + + logger.debug("Finished scanning and storing items for %s" % serviceName) + + # Adjust self at the end of the scan, so we don't have an incomplete set of + # data if an exception occurs during the scan. + self.servicesByName[serviceName] = service + self.servicesById[serviceId] = service + self.servicesByClass[service.service_class].append(service) + + return True + + def handler_value_changes(self, changes, path, senderId): + try: + service = self.servicesById[senderId] + a = service.paths[path] + except KeyError: + # Either senderId or path isn't there, which means + # it hasn't been scanned yet. + return + + # If this properyChange does not involve a value, our work is done. + if 'Value' not in changes: + return + + service.set_seen(path) + + # First update our store to the new value + changes['Value'] = unwrap_dbus_value(changes['Value']) + if a.value == changes['Value']: + return + + a.value = changes['Value'] + try: + a.text = changes['Text'] + except KeyError: + # Some services don't send Text with their PropertiesChanged events. + a.text = str(a.value) + + # And do the rest of the processing in on the mainloop + if self.valueChangedCallback is not None: + idle_add(exit_on_error, self._execute_value_changes, service.name, path, changes, a.options) + + def _execute_value_changes(self, serviceName, objectPath, changes, options): + # double check that the service still exists, as it might have + # disappeared between scheduling-for and executing this function. + if serviceName not in self.servicesByName: + return + + self.valueChangedCallback(serviceName, objectPath, + options, changes, self.get_device_instance(serviceName)) + + # Gets the value for a certain servicename and path + # The default_value is returned when: + # 1. When the service doesn't exist. + # 2. When the path asked for isn't being monitored. + # 3. When the path exists, but has dbus-invalid, ie an empty byte array. + # 4. When the path asked for is being monitored, but doesn't exist for that service. + def get_value(self, serviceName, objectPath, default_value=None): + service = self.servicesByName.get(serviceName, None) + if service is None: + return default_value + + value = service.paths.get(objectPath, None) + if value is None or value.value is None: + return default_value + + return value.value + + # returns if a dbus exists now, by doing a blocking dbus call. + # Typically seen will be sufficient and doesn't need access to the dbus. + def exists(self, serviceName, objectPath): + try: + self.dbusConn.call_blocking(serviceName, objectPath, None, 'GetValue', '', []) + return True + except dbus.exceptions.DBusException as e: + return False + + # Returns if there ever was a successful GetValue or valueChanged event. + # Unlike get_value this return True also if the actual value is invalid. + # + # Note: the path might no longer exists anymore, but that doesn't happen in + # practice. If a service really wants to reconfigure itself typically it should + # reconnect to the dbus which causes it to be rescanned and seen will be updated. + # If it is really needed to know if a path still exists, use exists. + def seen(self, serviceName, objectPath): + try: + return self.servicesByName[serviceName].seen(objectPath) + except KeyError: + return False + + # Sets the value for a certain servicename and path, returns the return value of the D-Bus SetValue + # method. If the underlying item does not exist (the service does not exist, or the objectPath was not + # registered) the function will return -1 + def set_value(self, serviceName, objectPath, value): + # Check if the D-Bus object referenced by serviceName and objectPath is registered. There is no + # necessity to do this, but it is in line with previous implementations which kept VeDbusItemImport + # objects for registers items only. + service = self.servicesByName.get(serviceName, None) + if service is None: + return -1 + if objectPath not in service.paths: + return -1 + # We do not catch D-Bus exceptions here, because the previous implementation did not do that either. + return self.dbusConn.call_blocking(serviceName, objectPath, + dbus_interface='com.victronenergy.BusItem', + method='SetValue', signature=None, + args=[wrap_dbus_value(value)]) + + # Similar to set_value, but operates asynchronously + def set_value_async(self, serviceName, objectPath, value, + reply_handler=None, error_handler=None): + service = self.servicesByName.get(serviceName, None) + if service is not None: + if objectPath in service.paths: + self.dbusConn.call_async(serviceName, objectPath, + dbus_interface='com.victronenergy.BusItem', + method='SetValue', signature=None, + args=[wrap_dbus_value(value)], + reply_handler=reply_handler, error_handler=error_handler) + return + + if error_handler is not None: + error_handler(TypeError('Service or path not found, ' + 'service=%s, path=%s' % (serviceName, objectPath))) + + # returns a dictionary, keys are the servicenames, value the instances + # optionally use the classfilter to get only a certain type of services, for + # example com.victronenergy.battery. + def get_service_list(self, classfilter=None): + if classfilter is None: + return { servicename: service.deviceInstance \ + for servicename, service in self.servicesByName.iteritems() } + + if classfilter not in self.servicesByClass: + return {} + + return { service.name: service.deviceInstance \ + for service in self.servicesByClass[classfilter] } + + def get_device_instance(self, serviceName): + return self.servicesByName[serviceName].deviceInstance + + # Parameter categoryfilter is to be a list, containing the categories you want (configChange, + # onIntervalAlways, etc). + # Returns a dictionary, keys are codes + instance, in VRM querystring format. For example vvt[0]. And + # values are the value. + def get_values(self, categoryfilter, converter=None): + + result = {} + + for serviceName in self.servicesByName: + result.update(self.get_values_for_service(categoryfilter, serviceName, converter)) + + return result + + # same as get_values above, but then for one service only + def get_values_for_service(self, categoryfilter, servicename, converter=None): + deviceInstance = self.get_device_instance(servicename) + result = {} + + service = self.servicesByName[servicename] + + for category in categoryfilter: + + for path in service[category]: + + value, text, options = service.paths[path] + + if value is not None: + + value = value if converter is None else converter.convert(path, options['code'], value, text) + + precision = options.get('precision') + if precision: + value = round(value, precision) + + result[options['code'] + "[" + str(deviceInstance) + "]"] = value + + return result + + def track_value(self, serviceName, objectPath, callback, *args, **kwargs): + """ A DbusMonitor can watch specific service/path combos for changes + so that it is not fully reliant on the global handler_value_changes + in this class. Additional watches are deleted automatically when + the service disappears from dbus. """ + self.serviceWatches[serviceName].append( + self.dbusConn.add_signal_receiver( + partial(callback, *args, **kwargs), + dbus_interface='com.victronenergy.BusItem', + signal_name='PropertiesChanged', + path=objectPath, bus_name=serviceName)) + + +# ====== ALL CODE BELOW THIS LINE IS PURELY FOR DEVELOPING THIS CLASS ====== + +# Example function that can be used as a starting point to use this code +def value_changed_on_dbus(dbusServiceName, dbusPath, dict, changes, deviceInstance): + logger.debug("0 ----------------") + logger.debug("1 %s%s changed" % (dbusServiceName, dbusPath)) + logger.debug("2 vrm dict : %s" % dict) + logger.debug("3 changes-text: %s" % changes['Text']) + logger.debug("4 changes-value: %s" % changes['Value']) + logger.debug("5 deviceInstance: %s" % deviceInstance) + logger.debug("6 - end") + + +def nameownerchange(a, b): + # used to find memory leaks in dbusmonitor and VeDbusItemImport + import gc + gc.collect() + objects = gc.get_objects() + print len([o for o in objects if type(o).__name__ == 'VeDbusItemImport']) + print len([o for o in objects if type(o).__name__ == 'SignalMatch']) + print len(objects) + + +def print_values(dbusmonitor): + a = dbusmonitor.get_value('wrongservice', '/DbusInvalid', default_value=1000) + b = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/NotInTheMonitorList', default_value=1000) + c = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/DbusInvalid', default_value=1000) + d = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/NonExistingButMonitored', default_value=1000) + + print "All should be 1000: Wrong Service: %s, NotInTheMonitorList: %s, DbusInvalid: %s, NonExistingButMonitored: %s" % (a, b, c, d) + return True + +# We have a mainloop, but that is just for developing this code. Normally above class & code is used from +# some other class, such as vrmLogger or the pubsub Implementation. +def main(): + # Init logging + logging.basicConfig(level=logging.DEBUG) + logger.info(__file__ + " is starting up") + + # Have a mainloop, so we can send/receive asynchronous calls to and from dbus + DBusGMainLoop(set_as_default=True) + + import os + import sys + sys.path.insert(1, os.path.join(os.path.dirname(__file__), '../../')) + + dummy = {'code': None, 'whenToLog': 'configChange', 'accessLevel': None} + monitorlist = {'com.victronenergy.dummyservice': { + '/Connected': dummy, + '/ProductName': dummy, + '/Mgmt/Connection': dummy, + '/Dc/0/Voltage': dummy, + '/Dc/0/Current': dummy, + '/Dc/0/Temperature': dummy, + '/Load/I': dummy, + '/FirmwareVersion': dummy, + '/DbusInvalid': dummy, + '/NonExistingButMonitored': dummy}} + + d = DbusMonitor(monitorlist, value_changed_on_dbus, + deviceAddedCallback=nameownerchange, deviceRemovedCallback=nameownerchange) + + # logger.info("==configchange values==") + # logger.info(pprint.pformat(d.get_values(['configChange']))) + + # logger.info("==onIntervalAlways and onIntervalOnlyWhenChanged==") + # logger.info(pprint.pformat(d.get_values(['onIntervalAlways', 'onIntervalAlwaysAndOnEvent']))) + + gobject.timeout_add(1000, print_values, d) + + # Start and run the mainloop + logger.info("Starting mainloop, responding on only events") + mainloop = gobject.MainLoop() + mainloop.run() + +if __name__ == "__main__": + main() diff --git a/velib_python/v2.73/oldestVersion b/velib_python/v2.73/oldestVersion new file mode 100644 index 0000000..c4da1d4 --- /dev/null +++ b/velib_python/v2.73/oldestVersion @@ -0,0 +1 @@ +v2.71 diff --git a/velib_python/v2.73/settingsdevice.py b/velib_python/v2.73/settingsdevice.py new file mode 100644 index 0000000..b525375 --- /dev/null +++ b/velib_python/v2.73/settingsdevice.py @@ -0,0 +1,115 @@ +import dbus +import logging +import time +from functools import partial + +# Local imports +from vedbus import VeDbusItemImport + +## Indexes for the setting dictonary. +PATH = 0 +VALUE = 1 +MINIMUM = 2 +MAXIMUM = 3 +SILENT = 4 + +## The Settings Device class. +# Used by python programs, such as the vrm-logger, to read and write settings they +# need to store on disk. And since these settings might be changed from a different +# source, such as the GUI, the program can pass an eventCallback that will be called +# as soon as some setting is changed. +# +# The settings are stored in flash via the com.victronenergy.settings service on dbus. +# See https://github.com/victronenergy/localsettings for more info. +# +# If there are settings in de supportSettings list which are not yet on the dbus, +# and therefore not yet in the xml file, they will be added through the dbus-addSetting +# interface of com.victronenergy.settings. +class SettingsDevice(object): + ## The constructor processes the tree of dbus-items. + # @param bus the system-dbus object + # @param name the dbus-service-name of the settings dbus service, 'com.victronenergy.settings' + # @param supportedSettings dictionary with all setting-names, and their defaultvalue, min, max and whether + # the setting is silent. The 'silent' entry is optional. If set to true, no changes in the setting will + # be logged by localsettings. + # @param eventCallback function that will be called on changes on any of these settings + # @param timeout Maximum interval to wait for localsettings. An exception is thrown at the end of the + # interval if the localsettings D-Bus service has not appeared yet. + def __init__(self, bus, supportedSettings, eventCallback, name='com.victronenergy.settings', timeout=0): + logging.debug("===== Settings device init starting... =====") + self._bus = bus + self._dbus_name = name + self._eventCallback = eventCallback + self._values = {} # stored the values, used to pass the old value along on a setting change + self._settings = {} + + count = 0 + while True: + if 'com.victronenergy.settings' in self._bus.list_names(): + break + if count == timeout: + raise Exception("The settings service com.victronenergy.settings does not exist!") + count += 1 + logging.info('waiting for settings') + time.sleep(1) + + # Add the items. + for setting, options in supportedSettings.items(): + silent = len(options) > SILENT and options[SILENT] + busitem = self.addSetting(options[PATH], options[VALUE], + options[MINIMUM], options[MAXIMUM], silent, callback=partial(self.handleChangedSetting, setting)) + self._settings[setting] = busitem + self._values[setting] = busitem.get_value() + + logging.debug("===== Settings device init finished =====") + + def addSetting(self, path, value, _min, _max, silent=False, callback=None): + busitem = VeDbusItemImport(self._bus, self._dbus_name, path, callback) + if busitem.exists and (value, _min, _max, silent) == busitem._proxy.GetAttributes(): + logging.debug("Setting %s found" % path) + else: + logging.info("Setting %s does not exist yet or must be adjusted" % path) + + # Prepare to add the setting. Most dbus types extend the python + # type so it is only necessary to additionally test for Int64. + if isinstance(value, (int, dbus.Int64)): + itemType = 'i' + elif isinstance(value, float): + itemType = 'f' + else: + itemType = 's' + + # Add the setting + # TODO, make an object that inherits VeDbusItemImport, and complete the D-Bus settingsitem interface + settings_item = VeDbusItemImport(self._bus, self._dbus_name, '/Settings', createsignal=False) + setting_path = path.replace('/Settings/', '', 1) + if silent: + settings_item._proxy.AddSilentSetting('', setting_path, value, itemType, _min, _max) + else: + settings_item._proxy.AddSetting('', setting_path, value, itemType, _min, _max) + + busitem = VeDbusItemImport(self._bus, self._dbus_name, path, callback) + + return busitem + + def handleChangedSetting(self, setting, servicename, path, changes): + oldvalue = self._values[setting] if setting in self._values else None + self._values[setting] = changes['Value'] + + if self._eventCallback is None: + return + + self._eventCallback(setting, oldvalue, changes['Value']) + + def setDefault(self, path): + item = VeDbusItemImport(self._bus, self._dbus_name, path, createsignal=False) + item.set_default() + + def __getitem__(self, setting): + return self._settings[setting].get_value() + + def __setitem__(self, setting, newvalue): + result = self._settings[setting].set_value(newvalue) + if result != 0: + # Trying to make some false change to our own settings? How dumb! + assert False diff --git a/velib_python/v2.73/ve_utils.py b/velib_python/v2.73/ve_utils.py new file mode 100644 index 0000000..c5cfb74 --- /dev/null +++ b/velib_python/v2.73/ve_utils.py @@ -0,0 +1,253 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from traceback import print_exc +from os import _exit as os_exit +from os import statvfs +from subprocess import check_output, CalledProcessError +import logging +import dbus +logger = logging.getLogger(__name__) + +VEDBUS_INVALID = dbus.Array([], signature=dbus.Signature('i'), variant_level=1) + +# Use this function to make sure the code quits on an unexpected exception. Make sure to use it +# when using gobject.idle_add and also gobject.timeout_add. +# Without this, the code will just keep running, since gobject does not stop the mainloop on an +# exception. +# Example: gobject.idle_add(exit_on_error, myfunc, arg1, arg2) +def exit_on_error(func, *args, **kwargs): + try: + return func(*args, **kwargs) + except: + try: + print 'exit_on_error: there was an exception. Printing stacktrace will be tried and then exit' + print_exc() + except: + pass + + # sys.exit() is not used, since that throws an exception, which does not lead to a program + # halt when used in a dbus callback, see connection.py in the Python/Dbus libraries, line 230. + os_exit(1) + + +__vrm_portal_id = None +def get_vrm_portal_id(): + # For the CCGX, the definition of the VRM Portal ID is that it is the mac address of the onboard- + # ethernet port (eth0), stripped from its colons (:) and lower case. + + # nice coincidence is that this also works fine when running on your (linux) development computer. + + global __vrm_portal_id + + if __vrm_portal_id: + return __vrm_portal_id + + # First try the method that works if we don't have a data partition. This will fail + # when the current user is not root. + try: + __vrm_portal_id = check_output("/sbin/get-unique-id").strip() + return __vrm_portal_id + except (CalledProcessError, OSError): + pass + + # Attempt to get the id from /data/venus/unique-id where venus puts it + # on startup. + try: + __vrm_portal_id = open('/data/venus/unique-id').read().strip() + except IOError: + pass + else: + return __vrm_portal_id + + # Fall back to getting our id using a syscall. Assume we are on linux. + # Allow the user to override what interface is used using an environment + # variable. + import fcntl, socket, struct, os + + iface = os.environ.get('VRM_IFACE', 'eth0') + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', iface[:15])) + __vrm_portal_id = ''.join(['%02x' % ord(char) for char in info[18:24]]) + return __vrm_portal_id + + +# See VE.Can registers - public.docx for definition of this conversion +def convert_vreg_version_to_readable(version): + def str_to_arr(x, length): + a = [] + for i in range(0, len(x), length): + a.append(x[i:i+length]) + return a + + x = "%x" % version + x = x.upper() + + if len(x) == 5 or len(x) == 3 or len(x) == 1: + x = '0' + x + + a = str_to_arr(x, 2); + + # remove the first 00 if there are three bytes and it is 00 + if len(a) == 3 and a[0] == '00': + a.remove(0); + + # if we have two or three bytes now, and the first character is a 0, remove it + if len(a) >= 2 and a[0][0:1] == '0': + a[0] = a[0][1]; + + result = '' + for item in a: + result += ('.' if result != '' else '') + item + + + result = 'v' + result + + return result + + +def get_free_space(path): + result = -1 + + try: + s = statvfs(path) + result = s.f_frsize * s.f_bavail # Number of free bytes that ordinary users + except Exception, ex: + logger.info("Error while retrieving free space for path %s: %s" % (path, ex)) + + return result + + +def get_load_averages(): + c = read_file('/proc/loadavg') + return c.split(' ')[:3] + + +def _get_sysfs_machine_name(): + try: + with open('/sys/firmware/devicetree/base/model', 'r') as f: + return f.read().rstrip('\x00') + except IOError: + pass + + return None + +# Returns None if it cannot find a machine name. Otherwise returns the string +# containing the name +def get_machine_name(): + # First try calling the venus utility script + try: + return check_output("/usr/bin/product-name").strip() + except (CalledProcessError, OSError): + pass + + # Fall back to sysfs + name = _get_sysfs_machine_name() + if name is not None: + return name + + # Fall back to venus build machine name + try: + with open('/etc/venus/machine', 'r') as f: + return f.read().strip() + except IOError: + pass + + return None + + +def get_product_id(): + """ Find the machine ID and return it. """ + + # First try calling the venus utility script + try: + return check_output("/usr/bin/product-id").strip() + except (CalledProcessError, OSError): + pass + + # Fall back machine name mechanism + name = _get_sysfs_machine_name() + return { + 'Color Control GX': 'C001', + 'Venus GX': 'C002', + 'Octo GX': 'C006', + 'EasySolar-II': 'C007', + 'MultiPlus-II': 'C008' + }.get(name, 'C003') # C003 is Generic + + +# Returns False if it cannot open the file. Otherwise returns its rstripped contents +def read_file(path): + content = False + + try: + with open(path, 'r') as f: + content = f.read().rstrip() + except Exception, ex: + logger.debug("Error while reading %s: %s" % (path, ex)) + + return content + + +def wrap_dbus_value(value): + if value is None: + return VEDBUS_INVALID + if isinstance(value, float): + return dbus.Double(value, variant_level=1) + if isinstance(value, bool): + return dbus.Boolean(value, variant_level=1) + if isinstance(value, int): + try: + return dbus.Int32(value, variant_level=1) + except OverflowError: + return dbus.Int64(value, variant_level=1) + if isinstance(value, str): + return dbus.String(value, variant_level=1) + if isinstance(value, unicode): + return dbus.String(value, variant_level=1) + if isinstance(value, list): + if len(value) == 0: + # If the list is empty we cannot infer the type of the contents. So assume unsigned integer. + # A (signed) integer is dangerous, because an empty list of signed integers is used to encode + # an invalid value. + return dbus.Array([], signature=dbus.Signature('u'), variant_level=1) + return dbus.Array([wrap_dbus_value(x) for x in value], variant_level=1) + if isinstance(value, long): + return dbus.Int64(value, variant_level=1) + if isinstance(value, dict): + # Wrapping the keys of the dictionary causes D-Bus errors like: + # 'arguments to dbus_message_iter_open_container() were incorrect, + # assertion "(type == DBUS_TYPE_ARRAY && contained_signature && + # *contained_signature == DBUS_DICT_ENTRY_BEGIN_CHAR) || (contained_signature == NULL || + # _dbus_check_is_valid_signature (contained_signature))" failed in file ...' + return dbus.Dictionary({(k, wrap_dbus_value(v)) for k, v in value.items()}, variant_level=1) + return value + + +dbus_int_types = (dbus.Int32, dbus.UInt32, dbus.Byte, dbus.Int16, dbus.UInt16, dbus.UInt32, dbus.Int64, dbus.UInt64) + + +def unwrap_dbus_value(val): + """Converts D-Bus values back to the original type. For example if val is of type DBus.Double, + a float will be returned.""" + if isinstance(val, dbus_int_types): + return int(val) + if isinstance(val, dbus.Double): + return float(val) + if isinstance(val, dbus.Array): + v = [unwrap_dbus_value(x) for x in val] + return None if len(v) == 0 else v + if isinstance(val, (dbus.Signature, dbus.String)): + return unicode(val) + # Python has no byte type, so we convert to an integer. + if isinstance(val, dbus.Byte): + return int(val) + if isinstance(val, dbus.ByteArray): + return "".join([str(x) for x in val]) + if isinstance(val, (list, tuple)): + return [unwrap_dbus_value(x) for x in val] + if isinstance(val, (dbus.Dictionary, dict)): + # Do not unwrap the keys, see comment in wrap_dbus_value + return dict([(x, unwrap_dbus_value(y)) for x, y in val.items()]) + if isinstance(val, dbus.Boolean): + return bool(val) + return val diff --git a/velib_python/v2.73/vedbus.py b/velib_python/v2.73/vedbus.py new file mode 100644 index 0000000..7fbe55d --- /dev/null +++ b/velib_python/v2.73/vedbus.py @@ -0,0 +1,501 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import dbus.service +import logging +import traceback +import os +import weakref +from ve_utils import wrap_dbus_value, unwrap_dbus_value + +# vedbus contains three classes: +# VeDbusItemImport -> use this to read data from the dbus, ie import +# VeDbusItemExport -> use this to export data to the dbus (one value) +# VeDbusService -> use that to create a service and export several values to the dbus + +# Code for VeDbusItemImport is copied from busitem.py and thereafter modified. +# All projects that used busitem.py need to migrate to this package. And some +# projects used to define there own equivalent of VeDbusItemExport. Better to +# use VeDbusItemExport, or even better the VeDbusService class that does it all for you. + +# TODOS +# 1 check for datatypes, it works now, but not sure if all is compliant with +# com.victronenergy.BusItem interface definition. See also the files in +# tests_and_examples. And see 'if type(v) == dbus.Byte:' on line 102. Perhaps +# something similar should also be done in VeDbusBusItemExport? +# 2 Shouldn't VeDbusBusItemExport inherit dbus.service.Object? +# 7 Make hard rules for services exporting data to the D-Bus, in order to make tracking +# changes possible. Does everybody first invalidate its data before leaving the bus? +# And what about before taking one object away from the bus, instead of taking the +# whole service offline? +# They should! And after taking one value away, do we need to know that someone left +# the bus? Or we just keep that value in invalidated for ever? Result is that we can't +# see the difference anymore between an invalidated value and a value that was first on +# the bus and later not anymore. See comments above VeDbusItemImport as well. +# 9 there are probably more todos in the code below. + +# Some thoughts with regards to the data types: +# +# Text from: http://dbus.freedesktop.org/doc/dbus-python/doc/tutorial.html#data-types +# --- +# Variants are represented by setting the variant_level keyword argument in the +# constructor of any D-Bus data type to a value greater than 0 (variant_level 1 +# means a variant containing some other data type, variant_level 2 means a variant +# containing a variant containing some other data type, and so on). If a non-variant +# is passed as an argument but introspection indicates that a variant is expected, +# it'll automatically be wrapped in a variant. +# --- +# +# Also the different dbus datatypes, such as dbus.Int32, and dbus.UInt32 are a subclass +# of Python int. dbus.String is a subclass of Python standard class unicode, etcetera +# +# So all together that explains why we don't need to explicitly convert back and forth +# between the dbus datatypes and the standard python datatypes. Note that all datatypes +# in python are objects. Even an int is an object. + +# The signature of a variant is 'v'. + +# Export ourselves as a D-Bus service. +class VeDbusService(object): + def __init__(self, servicename, bus=None): + # dict containing the VeDbusItemExport objects, with their path as the key. + self._dbusobjects = {} + self._dbusnodes = {} + + # dict containing the onchange callbacks, for each object. Object path is the key + self._onchangecallbacks = {} + + # Connect to session bus whenever present, else use the system bus + self._dbusconn = bus or (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else dbus.SystemBus()) + + # make the dbus connection available to outside, could make this a true property instead, but ach.. + self.dbusconn = self._dbusconn + + # Register ourselves on the dbus, trigger an error if already in use (do_not_queue) + self._dbusname = dbus.service.BusName(servicename, self._dbusconn, do_not_queue=True) + + # Add the root item that will return all items as a tree + self._dbusnodes['/'] = self._create_tree_export(self._dbusconn, '/', self._get_tree_dict) + + logging.info("registered ourselves on D-Bus as %s" % servicename) + + def _get_tree_dict(self, path, get_text=False): + logging.debug("_get_tree_dict called for %s" % path) + r = {} + px = path + if not px.endswith('/'): + px += '/' + for p, item in self._dbusobjects.items(): + if p.startswith(px): + v = item.GetText() if get_text else wrap_dbus_value(item.local_get_value()) + r[p[len(px):]] = v + logging.debug(r) + return r + + # To force immediate deregistering of this dbus service and all its object paths, explicitly + # call __del__(). + def __del__(self): + for node in self._dbusnodes.values(): + node.__del__() + self._dbusnodes.clear() + for item in self._dbusobjects.values(): + item.__del__() + self._dbusobjects.clear() + if self._dbusname: + self._dbusname.__del__() # Forces call to self._bus.release_name(self._name), see source code + self._dbusname = None + + # @param callbackonchange function that will be called when this value is changed. First parameter will + # be the path of the object, second the new value. This callback should return + # True to accept the change, False to reject it. + def add_path(self, path, value, description="", writeable=False, + onchangecallback=None, gettextcallback=None): + + if onchangecallback is not None: + self._onchangecallbacks[path] = onchangecallback + + item = VeDbusItemExport( + self._dbusconn, path, value, description, writeable, + self._value_changed, gettextcallback, deletecallback=self._item_deleted) + + spl = path.split('/') + for i in range(2, len(spl)): + subPath = '/'.join(spl[:i]) + if subPath not in self._dbusnodes and subPath not in self._dbusobjects: + self._dbusnodes[subPath] = self._create_tree_export(self._dbusconn, subPath, self._get_tree_dict) + self._dbusobjects[path] = item + logging.debug('added %s with start value %s. Writeable is %s' % (path, value, writeable)) + + # Add the mandatory paths, as per victron dbus api doc + def add_mandatory_paths(self, processname, processversion, connection, + deviceinstance, productid, productname, firmwareversion, hardwareversion, connected): + self.add_path('/Mgmt/ProcessName', processname) + self.add_path('/Mgmt/ProcessVersion', processversion) + self.add_path('/Mgmt/Connection', connection) + + # Create rest of the mandatory objects + self.add_path('/DeviceInstance', deviceinstance) + self.add_path('/ProductId', productid) + self.add_path('/ProductName', productname) + self.add_path('/FirmwareVersion', firmwareversion) + self.add_path('/HardwareVersion', hardwareversion) + self.add_path('/Connected', connected) + + def _create_tree_export(self, bus, objectPath, get_value_handler): + return VeDbusTreeExport(bus, objectPath, get_value_handler) + + # Callback function that is called from the VeDbusItemExport objects when a value changes. This function + # maps the change-request to the onchangecallback given to us for this specific path. + def _value_changed(self, path, newvalue): + if path not in self._onchangecallbacks: + return True + + return self._onchangecallbacks[path](path, newvalue) + + def _item_deleted(self, path): + self._dbusobjects.pop(path) + for np in self._dbusnodes.keys(): + if np != '/': + for ip in self._dbusobjects: + if ip.startswith(np + '/'): + break + else: + self._dbusnodes[np].__del__() + self._dbusnodes.pop(np) + + def __getitem__(self, path): + return self._dbusobjects[path].local_get_value() + + def __setitem__(self, path, newvalue): + self._dbusobjects[path].local_set_value(newvalue) + + def __delitem__(self, path): + self._dbusobjects[path].__del__() # Invalidates and then removes the object path + assert path not in self._dbusobjects + + def __contains__(self, path): + return path in self._dbusobjects + +""" +Importing basics: + - If when we power up, the D-Bus service does not exist, or it does exist and the path does not + yet exist, still subscribe to a signal: as soon as it comes online it will send a signal with its + initial value, which VeDbusItemImport will receive and use to update local cache. And, when set, + call the eventCallback. + - If when we power up, save it + - When using get_value, know that there is no difference between services (or object paths) that don't + exist and paths that are invalid (= empty array, see above). Both will return None. In case you do + really want to know ifa path exists or not, use the exists property. + - When a D-Bus service leaves the D-Bus, it will first invalidate all its values, and send signals + with that update, and only then leave the D-Bus. (or do we need to subscribe to the NameOwnerChanged- + signal!?!) To be discussed and make sure. Not really urgent, since all existing code that uses this + class already subscribes to the NameOwnerChanged signal, and subsequently removes instances of this + class. + +Read when using this class: +Note that when a service leaves that D-Bus without invalidating all its exported objects first, for +example because it is killed, VeDbusItemImport doesn't have a clue. So when using VeDbusItemImport, +make sure to also subscribe to the NamerOwnerChanged signal on bus-level. Or just use dbusmonitor, +because that takes care of all of that for you. +""" +class VeDbusItemImport(object): + ## Constructor + # @param bus the bus-object (SESSION or SYSTEM). + # @param serviceName the dbus-service-name (string), for example 'com.victronenergy.battery.ttyO1' + # @param path the object-path, for example '/Dc/V' + # @param eventCallback function that you want to be called on a value change + # @param createSignal only set this to False if you use this function to one time read a value. When + # leaving it to True, make sure to also subscribe to the NameOwnerChanged signal + # elsewhere. See also note some 15 lines up. + def __init__(self, bus, serviceName, path, eventCallback=None, createsignal=True): + # TODO: is it necessary to store _serviceName and _path? Isn't it + # stored in the bus_getobjectsomewhere? + self._serviceName = serviceName + self._path = path + self._match = None + # TODO: _proxy is being used in settingsdevice.py, make a getter for that + self._proxy = bus.get_object(serviceName, path, introspect=False) + self.eventCallback = eventCallback + + assert eventCallback is None or createsignal == True + if createsignal: + self._match = self._proxy.connect_to_signal( + "PropertiesChanged", weak_functor(self._properties_changed_handler)) + + # store the current value in _cachedvalue. When it doesn't exists set _cachedvalue to + # None, same as when a value is invalid + self._cachedvalue = None + try: + v = self._proxy.GetValue() + except dbus.exceptions.DBusException: + pass + else: + self._cachedvalue = unwrap_dbus_value(v) + + def __del__(self): + if self._match != None: + self._match.remove() + self._match = None + self._proxy = None + + def _refreshcachedvalue(self): + self._cachedvalue = unwrap_dbus_value(self._proxy.GetValue()) + + ## Returns the path as a string, for example '/AC/L1/V' + @property + def path(self): + return self._path + + ## Returns the dbus service name as a string, for example com.victronenergy.vebus.ttyO1 + @property + def serviceName(self): + return self._serviceName + + ## Returns the value of the dbus-item. + # the type will be a dbus variant, for example dbus.Int32(0, variant_level=1) + # this is not a property to keep the name consistant with the com.victronenergy.busitem interface + # returns None when the property is invalid + def get_value(self): + return self._cachedvalue + + ## Writes a new value to the dbus-item + def set_value(self, newvalue): + r = self._proxy.SetValue(wrap_dbus_value(newvalue)) + + # instead of just saving the value, go to the dbus and get it. So we have the right type etc. + if r == 0: + self._refreshcachedvalue() + + return r + + ## Resets the item to its default value + def set_default(self): + self._proxy.SetDefault() + self._refreshcachedvalue() + + ## Returns the text representation of the value. + # For example when the value is an enum/int GetText might return the string + # belonging to that enum value. Another example, for a voltage, GetValue + # would return a float, 12.0Volt, and GetText could return 12 VDC. + # + # Note that this depends on how the dbus-producer has implemented this. + def get_text(self): + return self._proxy.GetText() + + ## Returns true of object path exists, and false if it doesn't + @property + def exists(self): + # TODO: do some real check instead of this crazy thing. + r = False + try: + r = self._proxy.GetValue() + r = True + except dbus.exceptions.DBusException: + pass + + return r + + ## callback for the trigger-event. + # @param eventCallback the event-callback-function. + @property + def eventCallback(self): + return self._eventCallback + + @eventCallback.setter + def eventCallback(self, eventCallback): + self._eventCallback = eventCallback + + ## Is called when the value of the imported bus-item changes. + # Stores the new value in our local cache, and calls the eventCallback, if set. + def _properties_changed_handler(self, changes): + if "Value" in changes: + changes['Value'] = unwrap_dbus_value(changes['Value']) + self._cachedvalue = changes['Value'] + if self._eventCallback: + # The reason behind this try/except is to prevent errors silently ending up the an error + # handler in the dbus code. + try: + self._eventCallback(self._serviceName, self._path, changes) + except: + traceback.print_exc() + os._exit(1) # sys.exit() is not used, since that also throws an exception + + +class VeDbusTreeExport(dbus.service.Object): + def __init__(self, bus, objectPath, get_value_handler): + dbus.service.Object.__init__(self, bus, objectPath) + self._get_value_handler = get_value_handler + logging.debug("VeDbusTreeExport %s has been created" % objectPath) + + def __del__(self): + # self._get_path() will raise an exception when retrieved after the call to .remove_from_connection, + # so we need a copy. + path = self._get_path() + if path is None: + return + self.remove_from_connection() + logging.debug("VeDbusTreeExport %s has been removed" % path) + + def _get_path(self): + if len(self._locations) == 0: + return None + return self._locations[0][1] + + @dbus.service.method('com.victronenergy.BusItem', out_signature='v') + def GetValue(self): + value = self._get_value_handler(self._get_path()) + return dbus.Dictionary(value, signature=dbus.Signature('sv'), variant_level=1) + + @dbus.service.method('com.victronenergy.BusItem', out_signature='v') + def GetText(self): + return self._get_value_handler(self._get_path(), True) + + def local_get_value(self): + return self._get_value_handler(self.path) + + +class VeDbusItemExport(dbus.service.Object): + ## Constructor of VeDbusItemExport + # + # Use this object to export (publish), values on the dbus + # Creates the dbus-object under the given dbus-service-name. + # @param bus The dbus object. + # @param objectPath The dbus-object-path. + # @param value Value to initialize ourselves with, defaults to None which means Invalid + # @param description String containing a description. Can be called over the dbus with GetDescription() + # @param writeable what would this do!? :). + # @param callback Function that will be called when someone else changes the value of this VeBusItem + # over the dbus. First parameter passed to callback will be our path, second the new + # value. This callback should return True to accept the change, False to reject it. + def __init__(self, bus, objectPath, value=None, description=None, writeable=False, + onchangecallback=None, gettextcallback=None, deletecallback=None): + dbus.service.Object.__init__(self, bus, objectPath) + self._onchangecallback = onchangecallback + self._gettextcallback = gettextcallback + self._value = value + self._description = description + self._writeable = writeable + self._deletecallback = deletecallback + + # To force immediate deregistering of this dbus object, explicitly call __del__(). + def __del__(self): + # self._get_path() will raise an exception when retrieved after the + # call to .remove_from_connection, so we need a copy. + path = self._get_path() + if path == None: + return + if self._deletecallback is not None: + self._deletecallback(path) + self.local_set_value(None) + self.remove_from_connection() + logging.debug("VeDbusItemExport %s has been removed" % path) + + def _get_path(self): + if len(self._locations) == 0: + return None + return self._locations[0][1] + + ## Sets the value. And in case the value is different from what it was, a signal + # will be emitted to the dbus. This function is to be used in the python code that + # is using this class to export values to the dbus. + # set value to None to indicate that it is Invalid + def local_set_value(self, newvalue): + if self._value == newvalue: + return + + self._value = newvalue + + changes = {} + changes['Value'] = wrap_dbus_value(newvalue) + changes['Text'] = self.GetText() + self.PropertiesChanged(changes) + + def local_get_value(self): + return self._value + + # ==== ALL FUNCTIONS BELOW THIS LINE WILL BE CALLED BY OTHER PROCESSES OVER THE DBUS ==== + + ## Dbus exported method SetValue + # Function is called over the D-Bus by other process. It will first check (via callback) if new + # value is accepted. And it is, stores it and emits a changed-signal. + # @param value The new value. + # @return completion-code When successful a 0 is return, and when not a -1 is returned. + @dbus.service.method('com.victronenergy.BusItem', in_signature='v', out_signature='i') + def SetValue(self, newvalue): + if not self._writeable: + return 1 # NOT OK + + newvalue = unwrap_dbus_value(newvalue) + + if newvalue == self._value: + return 0 # OK + + # call the callback given to us, and check if new value is OK. + if (self._onchangecallback is None or + (self._onchangecallback is not None and self._onchangecallback(self.__dbus_object_path__, newvalue))): + + self.local_set_value(newvalue) + return 0 # OK + + return 2 # NOT OK + + ## Dbus exported method GetDescription + # + # Returns the a description. + # @param language A language code (e.g. ISO 639-1 en-US). + # @param length Lenght of the language string. + # @return description + @dbus.service.method('com.victronenergy.BusItem', in_signature='si', out_signature='s') + def GetDescription(self, language, length): + return self._description if self._description is not None else 'No description given' + + ## Dbus exported method GetValue + # Returns the value. + # @return the value when valid, and otherwise an empty array + @dbus.service.method('com.victronenergy.BusItem', out_signature='v') + def GetValue(self): + return wrap_dbus_value(self._value) + + ## Dbus exported method GetText + # Returns the value as string of the dbus-object-path. + # @return text A text-value. '---' when local value is invalid + @dbus.service.method('com.victronenergy.BusItem', out_signature='s') + def GetText(self): + if self._value is None: + return '---' + + # Default conversion from dbus.Byte will get you a character (so 'T' instead of '84'), so we + # have to convert to int first. Note that if a dbus.Byte turns up here, it must have come from + # the application itself, as all data from the D-Bus should have been unwrapped by now. + if self._gettextcallback is None and type(self._value) == dbus.Byte: + return str(int(self._value)) + + if self._gettextcallback is None and self.__dbus_object_path__ == '/ProductId': + return "0x%X" % self._value + + if self._gettextcallback is None: + return str(self._value) + + return self._gettextcallback(self.__dbus_object_path__, self._value) + + ## The signal that indicates that the value has changed. + # Other processes connected to this BusItem object will have subscribed to the + # event when they want to track our state. + @dbus.service.signal('com.victronenergy.BusItem', signature='a{sv}') + def PropertiesChanged(self, changes): + pass + +## This class behaves like a regular reference to a class method (eg. self.foo), but keeps a weak reference +## to the object which method is to be called. +## Use this object to break circular references. +class weak_functor: + def __init__(self, f): + self._r = weakref.ref(f.__self__) + self._f = weakref.ref(f.__func__) + + def __call__(self, *args, **kargs): + r = self._r() + f = self._f() + if r == None or f == None: + return + f(r, *args, **kargs) diff --git a/velib_python/v2.94/dbusmonitor.py b/velib_python/v2.94/dbusmonitor.py new file mode 100644 index 0000000..5f8e153 --- /dev/null +++ b/velib_python/v2.94/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 +notfound = object() # For lookups where None is a valid result + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) +class SystemBus(dbus.bus.BusConnection): + def __new__(cls): + return dbus.bus.BusConnection.__new__(cls, dbus.bus.BusConnection.TYPE_SYSTEM) + +class SessionBus(dbus.bus.BusConnection): + def __new__(cls): + return dbus.bus.BusConnection.__new__(cls, dbus.bus.BusConnection.TYPE_SESSION) + +class MonitoredValue(object): + def __init__(self, value, text, options): + super(MonitoredValue, self).__init__() + self.value = value + self.text = text + self.options = options + + # For legacy code, allow treating this as a tuple/list + def __iter__(self): + return iter((self.value, self.text, self.options)) + +class Service(object): + whentologoptions = ['configChange', 'onIntervalAlwaysAndOnEvent', + 'onIntervalOnlyWhenChanged', 'onIntervalAlways', 'never'] + def __init__(self, id, serviceName, deviceInstance): + super(Service, self).__init__() + self.id = id + self.name = serviceName + self.paths = {} + self._seen = set() + self.deviceInstance = deviceInstance + + self.configChange = [] + self.onIntervalAlwaysAndOnEvent = [] + self.onIntervalOnlyWhenChanged = [] + self.onIntervalAlways = [] + self.never = [] + + # For legacy code, attributes can still be accessed as if keys from a + # dictionary. + def __setitem__(self, key, value): + self.__dict__[key] = value + def __getitem__(self, key): + return self.__dict__[key] + + def set_seen(self, path): + self._seen.add(path) + + def seen(self, path): + return path in self._seen + + @property + def service_class(self): + return '.'.join(self.name.split('.')[:3]) + +class DbusMonitor(object): + ## Constructor + def __init__(self, dbusTree, valueChangedCallback=None, deviceAddedCallback=None, + deviceRemovedCallback=None, vebusDeviceInstance0=False): + # valueChangedCallback is the callback that we call when something has changed. + # def value_changed_on_dbus(dbusServiceName, dbusPath, options, changes, deviceInstance): + # in which changes is a tuple with GetText() and GetValue() + self.valueChangedCallback = valueChangedCallback + self.deviceAddedCallback = deviceAddedCallback + self.deviceRemovedCallback = deviceRemovedCallback + self.dbusTree = dbusTree + self.vebusDeviceInstance0 = vebusDeviceInstance0 + + # Lists all tracked services. Stores name, id, device instance, value per path, and whenToLog info + # indexed by service name (eg. com.victronenergy.settings). + self.servicesByName = {} + + # Same values as self.servicesByName, but indexed by service id (eg. :1.30) + self.servicesById = {} + + # Keep track of services by class to speed up calls to get_service_list + self.servicesByClass = defaultdict(list) + + # Keep track of any additional watches placed on items + self.serviceWatches = defaultdict(list) + + # For a PC, connect to the SessionBus + # For a CCGX, connect to the SystemBus + self.dbusConn = SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else SystemBus() + + # subscribe to NameOwnerChange for bus connect / disconnect events. + (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ \ + else dbus.SystemBus()).add_signal_receiver( + self.dbus_name_owner_changed, + signal_name='NameOwnerChanged') + + # Subscribe to PropertiesChanged for all services + self.dbusConn.add_signal_receiver(self.handler_value_changes, + dbus_interface='com.victronenergy.BusItem', + signal_name='PropertiesChanged', path_keyword='path', + sender_keyword='senderId') + + # Subscribe to ItemsChanged for all services + self.dbusConn.add_signal_receiver(self.handler_item_changes, + dbus_interface='com.victronenergy.BusItem', + signal_name='ItemsChanged', path='/', + sender_keyword='senderId') + + logger.info('===== Search on dbus for services that we will monitor starting... =====') + serviceNames = self.dbusConn.list_names() + for serviceName in serviceNames: + self.scan_dbus_service(serviceName) + + logger.info('===== Search on dbus for services that we will monitor finished =====') + + def dbus_name_owner_changed(self, name, oldowner, newowner): + if not name.startswith("com.victronenergy."): + return + + #decouple, and process in main loop + GLib.idle_add(exit_on_error, self._process_name_owner_changed, name, oldowner, newowner) + + def _process_name_owner_changed(self, name, oldowner, newowner): + if newowner != '': + # so we found some new service. Check if we can do something with it. + newdeviceadded = self.scan_dbus_service(name) + if newdeviceadded and self.deviceAddedCallback is not None: + self.deviceAddedCallback(name, self.get_device_instance(name)) + + elif name in self.servicesByName: + # it disappeared, we need to remove it. + logger.info("%s disappeared from the dbus. Removing it from our lists" % name) + service = self.servicesByName[name] + deviceInstance = service['deviceInstance'] + del self.servicesById[service.id] + del self.servicesByName[name] + for watch in self.serviceWatches[name]: + watch.remove() + del self.serviceWatches[name] + self.servicesByClass[service.service_class].remove(service) + if self.deviceRemovedCallback is not None: + self.deviceRemovedCallback(name, deviceInstance) + + def scan_dbus_service(self, serviceName): + try: + return self.scan_dbus_service_inner(serviceName) + except: + logger.error("Ignoring %s because of error while scanning:" % (serviceName)) + traceback.print_exc() + return False + + # Errors 'org.freedesktop.DBus.Error.ServiceUnknown' and + # 'org.freedesktop.DBus.Error.Disconnected' seem to happen when the service + # disappears while its being scanned. Which might happen, but is not really + # normal either, so letting them go into the logs. + + # Scans the given dbus service to see if it contains anything interesting for us. If it does, add + # it to our list of monitored D-Bus services. + def scan_dbus_service_inner(self, serviceName): + + # make it a normal string instead of dbus string + serviceName = str(serviceName) + + paths = self.dbusTree.get('.'.join(serviceName.split('.')[0:3]), None) + if paths is None: + logger.debug("Ignoring service %s, not in the tree" % serviceName) + return False + + logger.info("Found: %s, scanning and storing items" % serviceName) + serviceId = self.dbusConn.get_name_owner(serviceName) + + # we should never be notified to add a D-Bus service that we already have. If this assertion + # raises, check process_name_owner_changed, and D-Bus workings. + assert serviceName not in self.servicesByName + assert serviceId not in self.servicesById + + # for vebus.ttyO1, this is workaround, since VRM Portal expects the main vebus + # devices at instance 0. Not sure how to fix this yet. + if serviceName == 'com.victronenergy.vebus.ttyO1' and self.vebusDeviceInstance0: + di = 0 + elif serviceName == 'com.victronenergy.settings': + di = 0 + elif serviceName.startswith('com.victronenergy.vecan.'): + di = 0 + else: + try: + di = self.dbusConn.call_blocking(serviceName, + '/DeviceInstance', None, 'GetValue', '', []) + except dbus.exceptions.DBusException: + logger.info(" %s was skipped because it has no device instance" % serviceName) + return False # Skip it + else: + di = int(di) + + logger.info(" %s has device instance %s" % (serviceName, di)) + service = Service(serviceId, serviceName, di) + + # Let's try to fetch everything in one go + values = {} + texts = {} + try: + values.update(self.dbusConn.call_blocking(serviceName, '/', None, 'GetValue', '', [])) + texts.update(self.dbusConn.call_blocking(serviceName, '/', None, 'GetText', '', [])) + except: + pass + + for path, options in paths.items(): + # path will be the D-Bus path: '/Ac/ActiveIn/L1/V' + # options will be a dictionary: {'code': 'V', 'whenToLog': 'onIntervalAlways'} + # check that the whenToLog setting is set to something we expect + assert options['whenToLog'] is None or options['whenToLog'] in Service.whentologoptions + + # Try to obtain the value we want from our bulk fetch. If we + # cannot find it there, do an individual query. + value = values.get(path[1:], notfound) + if value != notfound: + service.set_seen(path) + text = texts.get(path[1:], notfound) + if value is notfound or text is notfound: + try: + value = self.dbusConn.call_blocking(serviceName, path, None, 'GetValue', '', []) + service.set_seen(path) + text = self.dbusConn.call_blocking(serviceName, path, None, 'GetText', '', []) + except dbus.exceptions.DBusException as e: + if e.get_dbus_name() in ( + 'org.freedesktop.DBus.Error.ServiceUnknown', + 'org.freedesktop.DBus.Error.Disconnected'): + raise # This exception will be handled below + + # TODO org.freedesktop.DBus.Error.UnknownMethod really + # shouldn't happen but sometimes does. + logger.debug("%s %s does not exist (yet)" % (serviceName, path)) + value = None + text = None + + service.paths[path] = MonitoredValue(unwrap_dbus_value(value), unwrap_dbus_value(text), options) + + if options['whenToLog']: + service[options['whenToLog']].append(path) + + + logger.debug("Finished scanning and storing items for %s" % serviceName) + + # Adjust self at the end of the scan, so we don't have an incomplete set of + # data if an exception occurs during the scan. + self.servicesByName[serviceName] = service + self.servicesById[serviceId] = service + self.servicesByClass[service.service_class].append(service) + + return True + + def handler_item_changes(self, items, senderId): + if not isinstance(items, dict): + return + + try: + service = self.servicesById[senderId] + except KeyError: + # senderId isn't there, which means it hasn't been scanned yet. + return + + for path, changes in items.items(): + try: + v = unwrap_dbus_value(changes['Value']) + except (KeyError, TypeError): + continue + + try: + t = changes['Text'] + except KeyError: + t = str(v) + self._handler_value_changes(service, path, v, t) + + def handler_value_changes(self, changes, path, senderId): + # If this properyChange does not involve a value, our work is done. + if 'Value' not in changes: + return + + try: + service = self.servicesById[senderId] + except KeyError: + # senderId isn't there, which means it hasn't been scanned yet. + return + + v = unwrap_dbus_value(changes['Value']) + # Some services don't send Text with their PropertiesChanged events. + try: + t = changes['Text'] + except KeyError: + t = str(v) + self._handler_value_changes(service, path, v, t) + + def _handler_value_changes(self, service, path, value, text): + try: + a = service.paths[path] + except KeyError: + # path isn't there, which means it hasn't been scanned yet. + return + + service.set_seen(path) + + # First update our store to the new value + if a.value == value: + return + + a.value = value + a.text = text + + # And do the rest of the processing in on the mainloop + if self.valueChangedCallback is not None: + GLib.idle_add(exit_on_error, self._execute_value_changes, service.name, path, { + 'Value': value, 'Text': text}, a.options) + + def _execute_value_changes(self, serviceName, objectPath, changes, options): + # double check that the service still exists, as it might have + # disappeared between scheduling-for and executing this function. + if serviceName not in self.servicesByName: + return + + self.valueChangedCallback(serviceName, objectPath, + options, changes, self.get_device_instance(serviceName)) + + # Gets the value for a certain servicename and path + # The default_value is returned when: + # 1. When the service doesn't exist. + # 2. When the path asked for isn't being monitored. + # 3. When the path exists, but has dbus-invalid, ie an empty byte array. + # 4. When the path asked for is being monitored, but doesn't exist for that service. + def get_value(self, serviceName, objectPath, default_value=None): + service = self.servicesByName.get(serviceName, None) + if service is None: + return default_value + + value = service.paths.get(objectPath, None) + if value is None or value.value is None: + return default_value + + return value.value + + # returns if a dbus exists now, by doing a blocking dbus call. + # Typically seen will be sufficient and doesn't need access to the dbus. + def exists(self, serviceName, objectPath): + try: + self.dbusConn.call_blocking(serviceName, objectPath, None, 'GetValue', '', []) + return True + except dbus.exceptions.DBusException as e: + return False + + # Returns if there ever was a successful GetValue or valueChanged event. + # Unlike get_value this return True also if the actual value is invalid. + # + # Note: the path might no longer exists anymore, but that doesn't happen in + # practice. If a service really wants to reconfigure itself typically it should + # reconnect to the dbus which causes it to be rescanned and seen will be updated. + # If it is really needed to know if a path still exists, use exists. + def seen(self, serviceName, objectPath): + try: + return self.servicesByName[serviceName].seen(objectPath) + except KeyError: + return False + + # Sets the value for a certain servicename and path, returns the return value of the D-Bus SetValue + # method. If the underlying item does not exist (the service does not exist, or the objectPath was not + # registered) the function will return -1 + def set_value(self, serviceName, objectPath, value): + # Check if the D-Bus object referenced by serviceName and objectPath is registered. There is no + # necessity to do this, but it is in line with previous implementations which kept VeDbusItemImport + # objects for registers items only. + service = self.servicesByName.get(serviceName, None) + if service is None: + return -1 + if objectPath not in service.paths: + return -1 + # We do not catch D-Bus exceptions here, because the previous implementation did not do that either. + return self.dbusConn.call_blocking(serviceName, objectPath, + dbus_interface='com.victronenergy.BusItem', + method='SetValue', signature=None, + args=[wrap_dbus_value(value)]) + + # Similar to set_value, but operates asynchronously + def set_value_async(self, serviceName, objectPath, value, + reply_handler=None, error_handler=None): + service = self.servicesByName.get(serviceName, None) + if service is not None: + if objectPath in service.paths: + self.dbusConn.call_async(serviceName, objectPath, + dbus_interface='com.victronenergy.BusItem', + method='SetValue', signature=None, + args=[wrap_dbus_value(value)], + reply_handler=reply_handler, error_handler=error_handler) + return + + if error_handler is not None: + error_handler(TypeError('Service or path not found, ' + 'service=%s, path=%s' % (serviceName, objectPath))) + + # returns a dictionary, keys are the servicenames, value the instances + # optionally use the classfilter to get only a certain type of services, for + # example com.victronenergy.battery. + def get_service_list(self, classfilter=None): + if classfilter is None: + return { servicename: service.deviceInstance \ + for servicename, service in self.servicesByName.items() } + + if classfilter not in self.servicesByClass: + return {} + + return { service.name: service.deviceInstance \ + for service in self.servicesByClass[classfilter] } + + def get_device_instance(self, serviceName): + return self.servicesByName[serviceName].deviceInstance + + # Parameter categoryfilter is to be a list, containing the categories you want (configChange, + # onIntervalAlways, etc). + # Returns a dictionary, keys are codes + instance, in VRM querystring format. For example vvt[0]. And + # values are the value. + def get_values(self, categoryfilter, converter=None): + + result = {} + + for serviceName in self.servicesByName: + result.update(self.get_values_for_service(categoryfilter, serviceName, converter)) + + return result + + # same as get_values above, but then for one service only + def get_values_for_service(self, categoryfilter, servicename, converter=None): + deviceInstance = self.get_device_instance(servicename) + result = {} + + service = self.servicesByName[servicename] + + for category in categoryfilter: + + for path in service[category]: + + value, text, options = service.paths[path] + + if value is not None: + + value = value if converter is None else converter.convert(path, options['code'], value, text) + + precision = options.get('precision') + if precision: + value = round(value, precision) + + result[options['code'] + "[" + str(deviceInstance) + "]"] = value + + return result + + def track_value(self, serviceName, objectPath, callback, *args, **kwargs): + """ A DbusMonitor can watch specific service/path combos for changes + so that it is not fully reliant on the global handler_value_changes + in this class. Additional watches are deleted automatically when + the service disappears from dbus. """ + cb = partial(callback, *args, **kwargs) + + def root_tracker(items): + # Check if objectPath in dict + try: + v = items[objectPath] + _v = unwrap_dbus_value(v['Value']) + except (KeyError, TypeError): + return # not in this dict + + try: + t = v['Text'] + except KeyError: + cb({'Value': _v }) + else: + cb({'Value': _v, 'Text': t}) + + # Track changes on the path, and also on root + self.serviceWatches[serviceName].extend(( + self.dbusConn.add_signal_receiver(cb, + dbus_interface='com.victronenergy.BusItem', + signal_name='PropertiesChanged', + path=objectPath, bus_name=serviceName), + self.dbusConn.add_signal_receiver(root_tracker, + dbus_interface='com.victronenergy.BusItem', + signal_name='ItemsChanged', + path="/", bus_name=serviceName), + )) + + +# ====== ALL CODE BELOW THIS LINE IS PURELY FOR DEVELOPING THIS CLASS ====== + +# Example function that can be used as a starting point to use this code +def value_changed_on_dbus(dbusServiceName, dbusPath, dict, changes, deviceInstance): + logger.debug("0 ----------------") + logger.debug("1 %s%s changed" % (dbusServiceName, dbusPath)) + logger.debug("2 vrm dict : %s" % dict) + logger.debug("3 changes-text: %s" % changes['Text']) + logger.debug("4 changes-value: %s" % changes['Value']) + logger.debug("5 deviceInstance: %s" % deviceInstance) + logger.debug("6 - end") + + +def nameownerchange(a, b): + # used to find memory leaks in dbusmonitor and VeDbusItemImport + import gc + gc.collect() + objects = gc.get_objects() + print (len([o for o in objects if type(o).__name__ == 'VeDbusItemImport'])) + print (len([o for o in objects if type(o).__name__ == 'SignalMatch'])) + print (len(objects)) + + +def print_values(dbusmonitor): + a = dbusmonitor.get_value('wrongservice', '/DbusInvalid', default_value=1000) + b = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/NotInTheMonitorList', default_value=1000) + c = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/DbusInvalid', default_value=1000) + d = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/NonExistingButMonitored', default_value=1000) + + print ("All should be 1000: Wrong Service: %s, NotInTheMonitorList: %s, DbusInvalid: %s, NonExistingButMonitored: %s" % (a, b, c, d)) + return True + +# We have a mainloop, but that is just for developing this code. Normally above class & code is used from +# some other class, such as vrmLogger or the pubsub Implementation. +def main(): + # Init logging + logging.basicConfig(level=logging.DEBUG) + logger.info(__file__ + " is starting up") + + # Have a mainloop, so we can send/receive asynchronous calls to and from dbus + DBusGMainLoop(set_as_default=True) + + import os + import sys + sys.path.insert(1, os.path.join(os.path.dirname(__file__), '../../')) + + dummy = {'code': None, 'whenToLog': 'configChange', 'accessLevel': None} + monitorlist = {'com.victronenergy.dummyservice': { + '/Connected': dummy, + '/ProductName': dummy, + '/Mgmt/Connection': dummy, + '/Dc/0/Voltage': dummy, + '/Dc/0/Current': dummy, + '/Dc/0/Temperature': dummy, + '/Load/I': dummy, + '/FirmwareVersion': dummy, + '/DbusInvalid': dummy, + '/NonExistingButMonitored': dummy}} + + d = DbusMonitor(monitorlist, value_changed_on_dbus, + deviceAddedCallback=nameownerchange, deviceRemovedCallback=nameownerchange) + + # logger.info("==configchange values==") + # logger.info(pprint.pformat(d.get_values(['configChange']))) + + # logger.info("==onIntervalAlways and onIntervalOnlyWhenChanged==") + # logger.info(pprint.pformat(d.get_values(['onIntervalAlways', 'onIntervalAlwaysAndOnEvent']))) + + GLib.timeout_add(1000, print_values, d) + + # Start and run the mainloop + logger.info("Starting mainloop, responding on only events") + mainloop = GLib.MainLoop() + mainloop.run() + +if __name__ == "__main__": + main() diff --git a/velib_python/v2.94/oldestVersion b/velib_python/v2.94/oldestVersion new file mode 100644 index 0000000..366edb3 --- /dev/null +++ b/velib_python/v2.94/oldestVersion @@ -0,0 +1 @@ +v2.80 diff --git a/velib_python/v2.94/settingsdevice.py b/velib_python/v2.94/settingsdevice.py new file mode 100644 index 0000000..a207e8b --- /dev/null +++ b/velib_python/v2.94/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/v2.94/ve_utils.py b/velib_python/v2.94/ve_utils.py new file mode 100644 index 0000000..e8d847d --- /dev/null +++ b/velib_python/v2.94/ve_utils.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import sys +from traceback import print_exc +from os import _exit as os_exit +from os import statvfs +from subprocess import check_output, CalledProcessError +import logging +import dbus +logger = logging.getLogger(__name__) + +VEDBUS_INVALID = dbus.Array([], signature=dbus.Signature('i'), variant_level=1) + +class NoVrmPortalIdError(Exception): + pass + +# Use this function to make sure the code quits on an unexpected exception. Make sure to use it +# when using GLib.idle_add and also GLib.timeout_add. +# Without this, the code will just keep running, since GLib does not stop the mainloop on an +# exception. +# Example: GLib.idle_add(exit_on_error, myfunc, arg1, arg2) +def exit_on_error(func, *args, **kwargs): + try: + return func(*args, **kwargs) + except: + try: + print ('exit_on_error: there was an exception. Printing stacktrace will be tried and then exit') + print_exc() + except: + pass + + # sys.exit() is not used, since that throws an exception, which does not lead to a program + # halt when used in a dbus callback, see connection.py in the Python/Dbus libraries, line 230. + os_exit(1) + + +__vrm_portal_id = None +def get_vrm_portal_id(): + # The original definition of the VRM Portal ID is that it is the mac + # address of the onboard- ethernet port (eth0), stripped from its colons + # (:) and lower case. This may however differ between platforms. On Venus + # the task is therefore deferred to /sbin/get-unique-id so that a + # platform specific method can be easily defined. + # + # If /sbin/get-unique-id does not exist, then use the ethernet address + # of eth0. This also handles the case where velib_python is used as a + # package install on a Raspberry Pi. + # + # On a Linux host where the network interface may not be eth0, you can set + # the VRM_IFACE environment variable to the correct name. + + global __vrm_portal_id + + if __vrm_portal_id: + return __vrm_portal_id + + portal_id = None + + # First try the method that works if we don't have a data partition. This + # will fail when the current user is not root. + try: + portal_id = check_output("/sbin/get-unique-id").decode("utf-8", "ignore").strip() + if not portal_id: + raise NoVrmPortalIdError("get-unique-id returned blank") + __vrm_portal_id = portal_id + return portal_id + except CalledProcessError: + # get-unique-id returned non-zero + raise NoVrmPortalIdError("get-unique-id returned non-zero") + except OSError: + # File doesn't exist, use fallback + pass + + # Fall back to getting our id using a syscall. Assume we are on linux. + # Allow the user to override what interface is used using an environment + # variable. + import fcntl, socket, struct, os + + iface = os.environ.get('VRM_IFACE', 'eth0').encode('ascii') + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', iface[:15])) + except IOError: + raise NoVrmPortalIdError("ioctl failed for eth0") + + __vrm_portal_id = info[18:24].hex() + return __vrm_portal_id + + +# See VE.Can registers - public.docx for definition of this conversion +def convert_vreg_version_to_readable(version): + def str_to_arr(x, length): + a = [] + for i in range(0, len(x), length): + a.append(x[i:i+length]) + return a + + x = "%x" % version + x = x.upper() + + if len(x) == 5 or len(x) == 3 or len(x) == 1: + x = '0' + x + + a = str_to_arr(x, 2); + + # remove the first 00 if there are three bytes and it is 00 + if len(a) == 3 and a[0] == '00': + a.remove(0); + + # if we have two or three bytes now, and the first character is a 0, remove it + if len(a) >= 2 and a[0][0:1] == '0': + a[0] = a[0][1]; + + result = '' + for item in a: + result += ('.' if result != '' else '') + item + + + result = 'v' + result + + return result + + +def get_free_space(path): + result = -1 + + try: + s = statvfs(path) + result = s.f_frsize * s.f_bavail # Number of free bytes that ordinary users + except Exception as ex: + logger.info("Error while retrieving free space for path %s: %s" % (path, ex)) + + return result + + +def get_load_averages(): + c = read_file('/proc/loadavg') + return c.split(' ')[:3] + + +def _get_sysfs_machine_name(): + try: + with open('/sys/firmware/devicetree/base/model', 'r') as f: + return f.read().rstrip('\x00') + except IOError: + pass + + return None + +# Returns None if it cannot find a machine name. Otherwise returns the string +# containing the name +def get_machine_name(): + # First try calling the venus utility script + try: + return check_output("/usr/bin/product-name").strip().decode('UTF-8') + except (CalledProcessError, OSError): + pass + + # Fall back to sysfs + name = _get_sysfs_machine_name() + if name is not None: + return name + + # Fall back to venus build machine name + try: + with open('/etc/venus/machine', 'r', encoding='UTF-8') as f: + return f.read().strip() + except IOError: + pass + + return None + + +def get_product_id(): + """ Find the machine ID and return it. """ + + # First try calling the venus utility script + try: + return check_output("/usr/bin/product-id").strip() + except (CalledProcessError, OSError): + pass + + # Fall back machine name mechanism + name = _get_sysfs_machine_name() + return { + 'Color Control GX': 'C001', + 'Venus GX': 'C002', + 'Octo GX': 'C006', + 'EasySolar-II': 'C007', + 'MultiPlus-II': 'C008' + }.get(name, 'C003') # C003 is Generic + + +# Returns False if it cannot open the file. Otherwise returns its rstripped contents +def read_file(path): + content = False + + try: + with open(path, 'r') as f: + content = f.read().rstrip() + except Exception as ex: + logger.debug("Error while reading %s: %s" % (path, ex)) + + return content + + +def wrap_dbus_value(value): + if value is None: + return VEDBUS_INVALID + if isinstance(value, float): + return dbus.Double(value, variant_level=1) + if isinstance(value, bool): + return dbus.Boolean(value, variant_level=1) + if isinstance(value, int): + try: + return dbus.Int32(value, variant_level=1) + except OverflowError: + return dbus.Int64(value, variant_level=1) + if isinstance(value, str): + return dbus.String(value, variant_level=1) + if isinstance(value, list): + if len(value) == 0: + # If the list is empty we cannot infer the type of the contents. So assume unsigned integer. + # A (signed) integer is dangerous, because an empty list of signed integers is used to encode + # an invalid value. + return dbus.Array([], signature=dbus.Signature('u'), variant_level=1) + return dbus.Array([wrap_dbus_value(x) for x in value], variant_level=1) + if isinstance(value, dict): + # Wrapping the keys of the dictionary causes D-Bus errors like: + # 'arguments to dbus_message_iter_open_container() were incorrect, + # assertion "(type == DBUS_TYPE_ARRAY && contained_signature && + # *contained_signature == DBUS_DICT_ENTRY_BEGIN_CHAR) || (contained_signature == NULL || + # _dbus_check_is_valid_signature (contained_signature))" failed in file ...' + return dbus.Dictionary({(k, wrap_dbus_value(v)) for k, v in value.items()}, variant_level=1) + return value + + +dbus_int_types = (dbus.Int32, dbus.UInt32, dbus.Byte, dbus.Int16, dbus.UInt16, dbus.UInt32, dbus.Int64, dbus.UInt64) + + +def unwrap_dbus_value(val): + """Converts D-Bus values back to the original type. For example if val is of type DBus.Double, + a float will be returned.""" + if isinstance(val, dbus_int_types): + return int(val) + if isinstance(val, dbus.Double): + return float(val) + if isinstance(val, dbus.Array): + v = [unwrap_dbus_value(x) for x in val] + return None if len(v) == 0 else v + if isinstance(val, (dbus.Signature, dbus.String)): + return str(val) + # Python has no byte type, so we convert to an integer. + if isinstance(val, dbus.Byte): + return int(val) + if isinstance(val, dbus.ByteArray): + return "".join([bytes(x) for x in val]) + if isinstance(val, (list, tuple)): + return [unwrap_dbus_value(x) for x in val] + if isinstance(val, (dbus.Dictionary, dict)): + # Do not unwrap the keys, see comment in wrap_dbus_value + return dict([(x, unwrap_dbus_value(y)) for x, y in val.items()]) + if isinstance(val, dbus.Boolean): + return bool(val) + return val diff --git a/velib_python/v2.94/vedbus.py b/velib_python/v2.94/vedbus.py new file mode 100644 index 0000000..d6dac60 --- /dev/null +++ b/velib_python/v2.94/vedbus.py @@ -0,0 +1,599 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import dbus.service +import logging +import traceback +import os +import weakref +from collections import defaultdict +from ve_utils import wrap_dbus_value, unwrap_dbus_value + +# vedbus contains three classes: +# VeDbusItemImport -> use this to read data from the dbus, ie import +# VeDbusItemExport -> use this to export data to the dbus (one value) +# VeDbusService -> use that to create a service and export several values to the dbus + +# Code for VeDbusItemImport is copied from busitem.py and thereafter modified. +# All projects that used busitem.py need to migrate to this package. And some +# projects used to define there own equivalent of VeDbusItemExport. Better to +# use VeDbusItemExport, or even better the VeDbusService class that does it all for you. + +# TODOS +# 1 check for datatypes, it works now, but not sure if all is compliant with +# com.victronenergy.BusItem interface definition. See also the files in +# tests_and_examples. And see 'if type(v) == dbus.Byte:' on line 102. Perhaps +# something similar should also be done in VeDbusBusItemExport? +# 2 Shouldn't VeDbusBusItemExport inherit dbus.service.Object? +# 7 Make hard rules for services exporting data to the D-Bus, in order to make tracking +# changes possible. Does everybody first invalidate its data before leaving the bus? +# And what about before taking one object away from the bus, instead of taking the +# whole service offline? +# They should! And after taking one value away, do we need to know that someone left +# the bus? Or we just keep that value in invalidated for ever? Result is that we can't +# see the difference anymore between an invalidated value and a value that was first on +# the bus and later not anymore. See comments above VeDbusItemImport as well. +# 9 there are probably more todos in the code below. + +# Some thoughts with regards to the data types: +# +# Text from: http://dbus.freedesktop.org/doc/dbus-python/doc/tutorial.html#data-types +# --- +# Variants are represented by setting the variant_level keyword argument in the +# constructor of any D-Bus data type to a value greater than 0 (variant_level 1 +# means a variant containing some other data type, variant_level 2 means a variant +# containing a variant containing some other data type, and so on). If a non-variant +# is passed as an argument but introspection indicates that a variant is expected, +# it'll automatically be wrapped in a variant. +# --- +# +# Also the different dbus datatypes, such as dbus.Int32, and dbus.UInt32 are a subclass +# of Python int. dbus.String is a subclass of Python standard class unicode, etcetera +# +# So all together that explains why we don't need to explicitly convert back and forth +# between the dbus datatypes and the standard python datatypes. Note that all datatypes +# in python are objects. Even an int is an object. + +# The signature of a variant is 'v'. + +# Export ourselves as a D-Bus service. +class VeDbusService(object): + def __init__(self, servicename, bus=None): + # dict containing the VeDbusItemExport objects, with their path as the key. + self._dbusobjects = {} + self._dbusnodes = {} + self._ratelimiters = [] + + # dict containing the onchange callbacks, for each object. Object path is the key + self._onchangecallbacks = {} + + # Connect to session bus whenever present, else use the system bus + self._dbusconn = bus or (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else dbus.SystemBus()) + + # make the dbus connection available to outside, could make this a true property instead, but ach.. + self.dbusconn = self._dbusconn + + # Register ourselves on the dbus, trigger an error if already in use (do_not_queue) + self._dbusname = dbus.service.BusName(servicename, self._dbusconn, do_not_queue=True) + + # Add the root item that will return all items as a tree + self._dbusnodes['/'] = VeDbusRootExport(self._dbusconn, '/', self) + + logging.info("registered ourselves on D-Bus as %s" % servicename) + + # To force immediate deregistering of this dbus service and all its object paths, explicitly + # call __del__(). + def __del__(self): + for node in list(self._dbusnodes.values()): + node.__del__() + self._dbusnodes.clear() + for item in list(self._dbusobjects.values()): + item.__del__() + self._dbusobjects.clear() + if self._dbusname: + self._dbusname.__del__() # Forces call to self._bus.release_name(self._name), see source code + self._dbusname = None + + # @param callbackonchange function that will be called when this value is changed. First parameter will + # be the path of the object, second the new value. This callback should return + # True to accept the change, False to reject it. + def add_path(self, path, value, description="", writeable=False, + onchangecallback=None, gettextcallback=None): + + if onchangecallback is not None: + self._onchangecallbacks[path] = onchangecallback + + item = VeDbusItemExport( + self._dbusconn, path, value, description, writeable, + self._value_changed, gettextcallback, deletecallback=self._item_deleted) + + spl = path.split('/') + for i in range(2, len(spl)): + subPath = '/'.join(spl[:i]) + if subPath not in self._dbusnodes and subPath not in self._dbusobjects: + self._dbusnodes[subPath] = VeDbusTreeExport(self._dbusconn, subPath, self) + self._dbusobjects[path] = item + logging.debug('added %s with start value %s. Writeable is %s' % (path, value, writeable)) + + # Add the mandatory paths, as per victron dbus api doc + def add_mandatory_paths(self, processname, processversion, connection, + deviceinstance, productid, productname, firmwareversion, hardwareversion, connected): + self.add_path('/Mgmt/ProcessName', processname) + self.add_path('/Mgmt/ProcessVersion', processversion) + self.add_path('/Mgmt/Connection', connection) + + # Create rest of the mandatory objects + self.add_path('/DeviceInstance', deviceinstance) + self.add_path('/ProductId', productid) + self.add_path('/ProductName', productname) + self.add_path('/FirmwareVersion', firmwareversion) + self.add_path('/HardwareVersion', hardwareversion) + self.add_path('/Connected', connected) + + # Callback function that is called from the VeDbusItemExport objects when a value changes. This function + # maps the change-request to the onchangecallback given to us for this specific path. + def _value_changed(self, path, newvalue): + if path not in self._onchangecallbacks: + return True + + return self._onchangecallbacks[path](path, newvalue) + + def _item_deleted(self, path): + self._dbusobjects.pop(path) + for np in list(self._dbusnodes.keys()): + if np != '/': + for ip in self._dbusobjects: + if ip.startswith(np + '/'): + break + else: + self._dbusnodes[np].__del__() + self._dbusnodes.pop(np) + + def __getitem__(self, path): + return self._dbusobjects[path].local_get_value() + + def __setitem__(self, path, newvalue): + self._dbusobjects[path].local_set_value(newvalue) + + def __delitem__(self, path): + self._dbusobjects[path].__del__() # Invalidates and then removes the object path + assert path not in self._dbusobjects + + def __contains__(self, path): + return path in self._dbusobjects + + def __enter__(self): + l = ServiceContext(self) + self._ratelimiters.append(l) + return l + + def __exit__(self, *exc): + # pop off the top one and flush it. If with statements are nested + # then each exit flushes its own part. + if self._ratelimiters: + self._ratelimiters.pop().flush() + +class ServiceContext(object): + def __init__(self, parent): + self.parent = parent + self.changes = {} + + def __getitem__(self, path): + return self.parent[path] + + def __setitem__(self, path, newvalue): + c = self.parent._dbusobjects[path]._local_set_value(newvalue) + if c is not None: + self.changes[path] = c + + def flush(self): + if self.changes: + self.parent._dbusnodes['/'].ItemsChanged(self.changes) + +class TrackerDict(defaultdict): + """ Same as defaultdict, but passes the key to default_factory. """ + def __missing__(self, key): + self[key] = x = self.default_factory(key) + return x + +class VeDbusRootTracker(object): + """ This tracks the root of a dbus path and listens for PropertiesChanged + signals. When a signal arrives, parse it and unpack the key/value changes + into traditional events, then pass it to the original eventCallback + method. """ + def __init__(self, bus, serviceName): + self.importers = defaultdict(weakref.WeakSet) + self.serviceName = serviceName + self._match = bus.get_object(serviceName, '/', introspect=False).connect_to_signal( + "ItemsChanged", weak_functor(self._items_changed_handler)) + + def __del__(self): + self._match.remove() + self._match = None + + def add(self, i): + self.importers[i.path].add(i) + + def _items_changed_handler(self, items): + if not isinstance(items, dict): + return + + for path, changes in items.items(): + try: + v = changes['Value'] + except KeyError: + continue + + try: + t = changes['Text'] + except KeyError: + t = str(unwrap_dbus_value(v)) + + for i in self.importers.get(path, ()): + i._properties_changed_handler({'Value': v, 'Text': t}) + +""" +Importing basics: + - If when we power up, the D-Bus service does not exist, or it does exist and the path does not + yet exist, still subscribe to a signal: as soon as it comes online it will send a signal with its + initial value, which VeDbusItemImport will receive and use to update local cache. And, when set, + call the eventCallback. + - If when we power up, save it + - When using get_value, know that there is no difference between services (or object paths) that don't + exist and paths that are invalid (= empty array, see above). Both will return None. In case you do + really want to know ifa path exists or not, use the exists property. + - When a D-Bus service leaves the D-Bus, it will first invalidate all its values, and send signals + with that update, and only then leave the D-Bus. (or do we need to subscribe to the NameOwnerChanged- + signal!?!) To be discussed and make sure. Not really urgent, since all existing code that uses this + class already subscribes to the NameOwnerChanged signal, and subsequently removes instances of this + class. + +Read when using this class: +Note that when a service leaves that D-Bus without invalidating all its exported objects first, for +example because it is killed, VeDbusItemImport doesn't have a clue. So when using VeDbusItemImport, +make sure to also subscribe to the NamerOwnerChanged signal on bus-level. Or just use dbusmonitor, +because that takes care of all of that for you. +""" +class VeDbusItemImport(object): + def __new__(cls, bus, serviceName, path, eventCallback=None, createsignal=True): + instance = object.__new__(cls) + + # If signal tracking should be done, also add to root tracker + if createsignal: + if "_roots" not in cls.__dict__: + cls._roots = TrackerDict(lambda k: VeDbusRootTracker(bus, k)) + + return instance + + ## Constructor + # @param bus the bus-object (SESSION or SYSTEM). + # @param serviceName the dbus-service-name (string), for example 'com.victronenergy.battery.ttyO1' + # @param path the object-path, for example '/Dc/V' + # @param eventCallback function that you want to be called on a value change + # @param createSignal only set this to False if you use this function to one time read a value. When + # leaving it to True, make sure to also subscribe to the NameOwnerChanged signal + # elsewhere. See also note some 15 lines up. + def __init__(self, bus, serviceName, path, eventCallback=None, createsignal=True): + # TODO: is it necessary to store _serviceName and _path? Isn't it + # stored in the bus_getobjectsomewhere? + self._serviceName = serviceName + self._path = path + self._match = None + # TODO: _proxy is being used in settingsdevice.py, make a getter for that + self._proxy = bus.get_object(serviceName, path, introspect=False) + self.eventCallback = eventCallback + + assert eventCallback is None or createsignal == True + if createsignal: + self._match = self._proxy.connect_to_signal( + "PropertiesChanged", weak_functor(self._properties_changed_handler)) + self._roots[serviceName].add(self) + + # store the current value in _cachedvalue. When it doesn't exists set _cachedvalue to + # None, same as when a value is invalid + self._cachedvalue = None + try: + v = self._proxy.GetValue() + except dbus.exceptions.DBusException: + pass + else: + self._cachedvalue = unwrap_dbus_value(v) + + def __del__(self): + if self._match is not None: + self._match.remove() + self._match = None + self._proxy = None + + def _refreshcachedvalue(self): + self._cachedvalue = unwrap_dbus_value(self._proxy.GetValue()) + + ## Returns the path as a string, for example '/AC/L1/V' + @property + def path(self): + return self._path + + ## Returns the dbus service name as a string, for example com.victronenergy.vebus.ttyO1 + @property + def serviceName(self): + return self._serviceName + + ## Returns the value of the dbus-item. + # the type will be a dbus variant, for example dbus.Int32(0, variant_level=1) + # this is not a property to keep the name consistant with the com.victronenergy.busitem interface + # returns None when the property is invalid + def get_value(self): + return self._cachedvalue + + ## Writes a new value to the dbus-item + def set_value(self, newvalue): + r = self._proxy.SetValue(wrap_dbus_value(newvalue)) + + # instead of just saving the value, go to the dbus and get it. So we have the right type etc. + if r == 0: + self._refreshcachedvalue() + + return r + + ## Resets the item to its default value + def set_default(self): + self._proxy.SetDefault() + self._refreshcachedvalue() + + ## Returns the text representation of the value. + # For example when the value is an enum/int GetText might return the string + # belonging to that enum value. Another example, for a voltage, GetValue + # would return a float, 12.0Volt, and GetText could return 12 VDC. + # + # Note that this depends on how the dbus-producer has implemented this. + def get_text(self): + return self._proxy.GetText() + + ## Returns true of object path exists, and false if it doesn't + @property + def exists(self): + # TODO: do some real check instead of this crazy thing. + r = False + try: + r = self._proxy.GetValue() + r = True + except dbus.exceptions.DBusException: + pass + + return r + + ## callback for the trigger-event. + # @param eventCallback the event-callback-function. + @property + def eventCallback(self): + return self._eventCallback + + @eventCallback.setter + def eventCallback(self, eventCallback): + self._eventCallback = eventCallback + + ## Is called when the value of the imported bus-item changes. + # Stores the new value in our local cache, and calls the eventCallback, if set. + def _properties_changed_handler(self, changes): + if "Value" in changes: + changes['Value'] = unwrap_dbus_value(changes['Value']) + self._cachedvalue = changes['Value'] + if self._eventCallback: + # The reason behind this try/except is to prevent errors silently ending up the an error + # handler in the dbus code. + try: + self._eventCallback(self._serviceName, self._path, changes) + except: + traceback.print_exc() + os._exit(1) # sys.exit() is not used, since that also throws an exception + + +class VeDbusTreeExport(dbus.service.Object): + def __init__(self, bus, objectPath, service): + dbus.service.Object.__init__(self, bus, objectPath) + self._service = service + logging.debug("VeDbusTreeExport %s has been created" % objectPath) + + def __del__(self): + # self._get_path() will raise an exception when retrieved after the call to .remove_from_connection, + # so we need a copy. + path = self._get_path() + if path is None: + return + self.remove_from_connection() + logging.debug("VeDbusTreeExport %s has been removed" % path) + + def _get_path(self): + if len(self._locations) == 0: + return None + return self._locations[0][1] + + def _get_value_handler(self, path, get_text=False): + logging.debug("_get_value_handler called for %s" % path) + r = {} + px = path + if not px.endswith('/'): + px += '/' + for p, item in self._service._dbusobjects.items(): + if p.startswith(px): + v = item.GetText() if get_text else wrap_dbus_value(item.local_get_value()) + r[p[len(px):]] = v + logging.debug(r) + return r + + @dbus.service.method('com.victronenergy.BusItem', out_signature='v') + def GetValue(self): + value = self._get_value_handler(self._get_path()) + return dbus.Dictionary(value, signature=dbus.Signature('sv'), variant_level=1) + + @dbus.service.method('com.victronenergy.BusItem', out_signature='v') + def GetText(self): + return self._get_value_handler(self._get_path(), True) + + def local_get_value(self): + return self._get_value_handler(self.path) + +class VeDbusRootExport(VeDbusTreeExport): + @dbus.service.signal('com.victronenergy.BusItem', signature='a{sa{sv}}') + def ItemsChanged(self, changes): + pass + + @dbus.service.method('com.victronenergy.BusItem', out_signature='a{sa{sv}}') + def GetItems(self): + return { + path: { + 'Value': wrap_dbus_value(item.local_get_value()), + 'Text': item.GetText() } + for path, item in self._service._dbusobjects.items() + } + + +class VeDbusItemExport(dbus.service.Object): + ## Constructor of VeDbusItemExport + # + # Use this object to export (publish), values on the dbus + # Creates the dbus-object under the given dbus-service-name. + # @param bus The dbus object. + # @param objectPath The dbus-object-path. + # @param value Value to initialize ourselves with, defaults to None which means Invalid + # @param description String containing a description. Can be called over the dbus with GetDescription() + # @param writeable what would this do!? :). + # @param callback Function that will be called when someone else changes the value of this VeBusItem + # over the dbus. First parameter passed to callback will be our path, second the new + # value. This callback should return True to accept the change, False to reject it. + def __init__(self, bus, objectPath, value=None, description=None, writeable=False, + onchangecallback=None, gettextcallback=None, deletecallback=None): + dbus.service.Object.__init__(self, bus, objectPath) + self._onchangecallback = onchangecallback + self._gettextcallback = gettextcallback + self._value = value + self._description = description + self._writeable = writeable + self._deletecallback = deletecallback + + # To force immediate deregistering of this dbus object, explicitly call __del__(). + def __del__(self): + # self._get_path() will raise an exception when retrieved after the + # call to .remove_from_connection, so we need a copy. + path = self._get_path() + if path == None: + return + if self._deletecallback is not None: + self._deletecallback(path) + self.local_set_value(None) + self.remove_from_connection() + logging.debug("VeDbusItemExport %s has been removed" % path) + + def _get_path(self): + if len(self._locations) == 0: + return None + return self._locations[0][1] + + ## Sets the value. And in case the value is different from what it was, a signal + # will be emitted to the dbus. This function is to be used in the python code that + # is using this class to export values to the dbus. + # set value to None to indicate that it is Invalid + def local_set_value(self, newvalue): + changes = self._local_set_value(newvalue) + if changes is not None: + self.PropertiesChanged(changes) + + def _local_set_value(self, newvalue): + if self._value == newvalue: + return None + + self._value = newvalue + return { + 'Value': wrap_dbus_value(newvalue), + 'Text': self.GetText() + } + + def local_get_value(self): + return self._value + + # ==== ALL FUNCTIONS BELOW THIS LINE WILL BE CALLED BY OTHER PROCESSES OVER THE DBUS ==== + + ## Dbus exported method SetValue + # Function is called over the D-Bus by other process. It will first check (via callback) if new + # value is accepted. And it is, stores it and emits a changed-signal. + # @param value The new value. + # @return completion-code When successful a 0 is return, and when not a -1 is returned. + @dbus.service.method('com.victronenergy.BusItem', in_signature='v', out_signature='i') + def SetValue(self, newvalue): + if not self._writeable: + return 1 # NOT OK + + newvalue = unwrap_dbus_value(newvalue) + + if newvalue == self._value: + return 0 # OK + + # call the callback given to us, and check if new value is OK. + if (self._onchangecallback is None or + (self._onchangecallback is not None and self._onchangecallback(self.__dbus_object_path__, newvalue))): + + self.local_set_value(newvalue) + return 0 # OK + + return 2 # NOT OK + + ## Dbus exported method GetDescription + # + # Returns the a description. + # @param language A language code (e.g. ISO 639-1 en-US). + # @param length Lenght of the language string. + # @return description + @dbus.service.method('com.victronenergy.BusItem', in_signature='si', out_signature='s') + def GetDescription(self, language, length): + return self._description if self._description is not None else 'No description given' + + ## Dbus exported method GetValue + # Returns the value. + # @return the value when valid, and otherwise an empty array + @dbus.service.method('com.victronenergy.BusItem', out_signature='v') + def GetValue(self): + return wrap_dbus_value(self._value) + + ## Dbus exported method GetText + # Returns the value as string of the dbus-object-path. + # @return text A text-value. '---' when local value is invalid + @dbus.service.method('com.victronenergy.BusItem', out_signature='s') + def GetText(self): + if self._value is None: + return '---' + + # Default conversion from dbus.Byte will get you a character (so 'T' instead of '84'), so we + # have to convert to int first. Note that if a dbus.Byte turns up here, it must have come from + # the application itself, as all data from the D-Bus should have been unwrapped by now. + if self._gettextcallback is None and type(self._value) == dbus.Byte: + return str(int(self._value)) + + if self._gettextcallback is None and self.__dbus_object_path__ == '/ProductId': + return "0x%X" % self._value + + if self._gettextcallback is None: + return str(self._value) + + return self._gettextcallback(self.__dbus_object_path__, self._value) + + ## The signal that indicates that the value has changed. + # Other processes connected to this BusItem object will have subscribed to the + # event when they want to track our state. + @dbus.service.signal('com.victronenergy.BusItem', signature='a{sv}') + def PropertiesChanged(self, changes): + pass + +## This class behaves like a regular reference to a class method (eg. self.foo), but keeps a weak reference +## to the object which method is to be called. +## Use this object to break circular references. +class weak_functor: + def __init__(self, f): + self._r = weakref.ref(f.__self__) + self._f = weakref.ref(f.__func__) + + def __call__(self, *args, **kargs): + r = self._r() + f = self._f() + if r == None or f == None: + return + f(r, *args, **kargs) diff --git a/velib_python/v3.40~37/dbusmonitor.py b/velib_python/v3.40~37/dbusmonitor.py new file mode 100644 index 0000000..cb2185d --- /dev/null +++ b/velib_python/v3.40~37/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/v3.40~37/oldestVersion b/velib_python/v3.40~37/oldestVersion new file mode 100644 index 0000000..3f6c1a0 --- /dev/null +++ b/velib_python/v3.40~37/oldestVersion @@ -0,0 +1 @@ +v3.00~32 diff --git a/velib_python/v3.40~37/settingsdevice.py b/velib_python/v3.40~37/settingsdevice.py new file mode 100644 index 0000000..a207e8b --- /dev/null +++ b/velib_python/v3.40~37/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/v3.40~37/ve_utils.py b/velib_python/v3.40~37/ve_utils.py new file mode 100644 index 0000000..63a915b --- /dev/null +++ b/velib_python/v3.40~37/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/v3.40~37/vedbus.py b/velib_python/v3.40~37/vedbus.py new file mode 100644 index 0000000..8c101ea --- /dev/null +++ b/velib_python/v3.40~37/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/v3.40~38/dbusmonitor.py b/velib_python/v3.40~38/dbusmonitor.py new file mode 100644 index 0000000..fd25700 --- /dev/null +++ b/velib_python/v3.40~38/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/v3.40~38/oldestVersion b/velib_python/v3.40~38/oldestVersion new file mode 100644 index 0000000..8aa0dc8 --- /dev/null +++ b/velib_python/v3.40~38/oldestVersion @@ -0,0 +1 @@ +v3.40~38 diff --git a/velib_python/v3.40~38/settingsdevice.py b/velib_python/v3.40~38/settingsdevice.py new file mode 100644 index 0000000..a207e8b --- /dev/null +++ b/velib_python/v3.40~38/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/v3.40~38/ve_utils.py b/velib_python/v3.40~38/ve_utils.py new file mode 100644 index 0000000..f5a2f85 --- /dev/null +++ b/velib_python/v3.40~38/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/v3.40~38/vedbus.py b/velib_python/v3.40~38/vedbus.py new file mode 100644 index 0000000..0407f6c --- /dev/null +++ b/velib_python/v3.40~38/vedbus.py @@ -0,0 +1,642 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import dbus.service +import logging +import traceback +import os +import weakref +from collections import defaultdict +from ve_utils import wrap_dbus_value, unwrap_dbus_value + +# vedbus contains three classes: +# VeDbusItemImport -> use this to read data from the dbus, ie import +# VeDbusItemExport -> use this to export data to the dbus (one value) +# VeDbusService -> use that to create a service and export several values to the dbus + +# Code for VeDbusItemImport is copied from busitem.py and thereafter modified. +# All projects that used busitem.py need to migrate to this package. And some +# projects used to define there own equivalent of VeDbusItemExport. Better to +# use VeDbusItemExport, or even better the VeDbusService class that does it all for you. + +# TODOS +# 1 check for datatypes, it works now, but not sure if all is compliant with +# com.victronenergy.BusItem interface definition. See also the files in +# tests_and_examples. And see 'if type(v) == dbus.Byte:' on line 102. Perhaps +# something similar should also be done in VeDbusBusItemExport? +# 2 Shouldn't VeDbusBusItemExport inherit dbus.service.Object? +# 7 Make hard rules for services exporting data to the D-Bus, in order to make tracking +# changes possible. Does everybody first invalidate its data before leaving the bus? +# And what about before taking one object away from the bus, instead of taking the +# whole service offline? +# They should! And after taking one value away, do we need to know that someone left +# the bus? Or we just keep that value in invalidated for ever? Result is that we can't +# see the difference anymore between an invalidated value and a value that was first on +# the bus and later not anymore. See comments above VeDbusItemImport as well. +# 9 there are probably more todos in the code below. + +# Some thoughts with regards to the data types: +# +# Text from: http://dbus.freedesktop.org/doc/dbus-python/doc/tutorial.html#data-types +# --- +# Variants are represented by setting the variant_level keyword argument in the +# constructor of any D-Bus data type to a value greater than 0 (variant_level 1 +# means a variant containing some other data type, variant_level 2 means a variant +# containing a variant containing some other data type, and so on). If a non-variant +# is passed as an argument but introspection indicates that a variant is expected, +# it'll automatically be wrapped in a variant. +# --- +# +# Also the different dbus datatypes, such as dbus.Int32, and dbus.UInt32 are a subclass +# of Python int. dbus.String is a subclass of Python standard class unicode, etcetera +# +# So all together that explains why we don't need to explicitly convert back and forth +# between the dbus datatypes and the standard python datatypes. Note that all datatypes +# in python are objects. Even an int is an object. + +# The signature of a variant is 'v'. + +# Export ourselves as a D-Bus service. +class VeDbusService(object): + def __init__(self, servicename, bus=None): + # dict containing the VeDbusItemExport objects, with their path as the key. + self._dbusobjects = {} + self._dbusnodes = {} + self._ratelimiters = [] + self._dbusname = None + self.name = servicename + + # dict containing the onchange callbacks, for each object. Object path is the key + self._onchangecallbacks = {} + + # Connect to session bus whenever present, else use the system bus + self._dbusconn = bus or (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else dbus.SystemBus()) + + # make the dbus connection available to outside, could make this a true property instead, but ach.. + self.dbusconn = self._dbusconn + + # Add the root item that will return all items as a tree + self._dbusnodes['/'] = VeDbusRootExport(self._dbusconn, '/', self) + + def register(self): + # Register ourselves on the dbus, trigger an error if already in use (do_not_queue) + self._dbusname = dbus.service.BusName(self.name, self._dbusconn, do_not_queue=True) + logging.info("registered ourselves on D-Bus as %s" % self.name) + + # To force immediate deregistering of this dbus service and all its object paths, explicitly + # call __del__(). + def __del__(self): + for node in list(self._dbusnodes.values()): + node.__del__() + self._dbusnodes.clear() + for item in list(self._dbusobjects.values()): + item.__del__() + self._dbusobjects.clear() + if self._dbusname: + self._dbusname.__del__() # Forces call to self._bus.release_name(self._name), see source code + self._dbusname = None + + def get_name(self): + return self._dbusname.get_name() + + # @param callbackonchange function that will be called when this value is changed. First parameter will + # be the path of the object, second the new value. This callback should return + # True to accept the change, False to reject it. + def add_path(self, path, value, description="", writeable=False, + onchangecallback=None, gettextcallback=None, valuetype=None, itemtype=None): + + if onchangecallback is not None: + self._onchangecallbacks[path] = onchangecallback + + itemtype = itemtype or VeDbusItemExport + item = itemtype(self._dbusconn, path, value, description, writeable, + self._value_changed, gettextcallback, deletecallback=self._item_deleted, valuetype=valuetype) + + spl = path.split('/') + for i in range(2, len(spl)): + subPath = '/'.join(spl[:i]) + if subPath not in self._dbusnodes and subPath not in self._dbusobjects: + self._dbusnodes[subPath] = VeDbusTreeExport(self._dbusconn, subPath, self) + self._dbusobjects[path] = item + logging.debug('added %s with start value %s. Writeable is %s' % (path, value, writeable)) + return item + + # Add the mandatory paths, as per victron dbus api doc + def add_mandatory_paths(self, processname, processversion, connection, + deviceinstance, productid, productname, firmwareversion, hardwareversion, connected): + self.add_path('/Mgmt/ProcessName', processname) + self.add_path('/Mgmt/ProcessVersion', processversion) + self.add_path('/Mgmt/Connection', connection) + + # Create rest of the mandatory objects + self.add_path('/DeviceInstance', deviceinstance) + self.add_path('/ProductId', productid) + self.add_path('/ProductName', productname) + self.add_path('/FirmwareVersion', firmwareversion) + self.add_path('/HardwareVersion', hardwareversion) + self.add_path('/Connected', connected) + + # Callback function that is called from the VeDbusItemExport objects when a value changes. This function + # maps the change-request to the onchangecallback given to us for this specific path. + def _value_changed(self, path, newvalue): + if path not in self._onchangecallbacks: + return True + + return self._onchangecallbacks[path](path, newvalue) + + def _item_deleted(self, path): + self._dbusobjects.pop(path) + for np in list(self._dbusnodes.keys()): + if np != '/': + for ip in self._dbusobjects: + if ip.startswith(np + '/'): + break + else: + self._dbusnodes[np].__del__() + self._dbusnodes.pop(np) + + def __getitem__(self, path): + return self._dbusobjects[path].local_get_value() + + def __setitem__(self, path, newvalue): + self._dbusobjects[path].local_set_value(newvalue) + + def __delitem__(self, path): + self._dbusobjects[path].__del__() # Invalidates and then removes the object path + assert path not in self._dbusobjects + + def __contains__(self, path): + return path in self._dbusobjects + + def __enter__(self): + l = ServiceContext(self) + self._ratelimiters.append(l) + return l + + def __exit__(self, *exc): + # pop off the top one and flush it. If with statements are nested + # then each exit flushes its own part. + if self._ratelimiters: + self._ratelimiters.pop().flush() + +class ServiceContext(object): + def __init__(self, parent): + self.parent = parent + self.changes = {} + + def __contains__(self, path): + return path in self.parent + + def __getitem__(self, path): + return self.parent[path] + + def __setitem__(self, path, newvalue): + c = self.parent._dbusobjects[path]._local_set_value(newvalue) + if c is not None: + self.changes[path] = c + + def __delitem__(self, path): + if path in self.changes: + del self.changes[path] + del self.parent[path] + + def flush(self): + if self.changes: + self.parent._dbusnodes['/'].ItemsChanged(self.changes) + self.changes.clear() + + def add_path(self, path, value, *args, **kwargs): + self.parent.add_path(path, value, *args, **kwargs) + self.changes[path] = { + 'Value': wrap_dbus_value(value), + 'Text': self.parent._dbusobjects[path].GetText() + } + + def del_tree(self, root): + root = root.rstrip('/') + for p in list(self.parent._dbusobjects.keys()): + if p == root or p.startswith(root + '/'): + self[p] = None + self.parent._dbusobjects[p].__del__() + + def get_name(self): + return self.parent.get_name() + +class TrackerDict(defaultdict): + """ Same as defaultdict, but passes the key to default_factory. """ + def __missing__(self, key): + self[key] = x = self.default_factory(key) + return x + +class VeDbusRootTracker(object): + """ This tracks the root of a dbus path and listens for PropertiesChanged + signals. When a signal arrives, parse it and unpack the key/value changes + into traditional events, then pass it to the original eventCallback + method. """ + def __init__(self, bus, serviceName): + self.importers = defaultdict(weakref.WeakSet) + self.serviceName = serviceName + self._match = bus.get_object(serviceName, '/', introspect=False).connect_to_signal( + "ItemsChanged", weak_functor(self._items_changed_handler)) + + def __del__(self): + self._match.remove() + self._match = None + + def add(self, i): + self.importers[i.path].add(i) + + def _items_changed_handler(self, items): + if not isinstance(items, dict): + return + + for path, changes in items.items(): + try: + v = changes['Value'] + except KeyError: + continue + + try: + t = changes['Text'] + except KeyError: + t = str(unwrap_dbus_value(v)) + + for i in self.importers.get(path, ()): + i._properties_changed_handler({'Value': v, 'Text': t}) + +""" +Importing basics: + - If when we power up, the D-Bus service does not exist, or it does exist and the path does not + yet exist, still subscribe to a signal: as soon as it comes online it will send a signal with its + initial value, which VeDbusItemImport will receive and use to update local cache. And, when set, + call the eventCallback. + - If when we power up, save it + - When using get_value, know that there is no difference between services (or object paths) that don't + exist and paths that are invalid (= empty array, see above). Both will return None. In case you do + really want to know ifa path exists or not, use the exists property. + - When a D-Bus service leaves the D-Bus, it will first invalidate all its values, and send signals + with that update, and only then leave the D-Bus. (or do we need to subscribe to the NameOwnerChanged- + signal!?!) To be discussed and make sure. Not really urgent, since all existing code that uses this + class already subscribes to the NameOwnerChanged signal, and subsequently removes instances of this + class. + +Read when using this class: +Note that when a service leaves that D-Bus without invalidating all its exported objects first, for +example because it is killed, VeDbusItemImport doesn't have a clue. So when using VeDbusItemImport, +make sure to also subscribe to the NamerOwnerChanged signal on bus-level. Or just use dbusmonitor, +because that takes care of all of that for you. +""" +class VeDbusItemImport(object): + def __new__(cls, bus, serviceName, path, eventCallback=None, createsignal=True): + instance = object.__new__(cls) + + # If signal tracking should be done, also add to root tracker + if createsignal: + if "_roots" not in cls.__dict__: + cls._roots = TrackerDict(lambda k: VeDbusRootTracker(bus, k)) + + return instance + + ## Constructor + # @param bus the bus-object (SESSION or SYSTEM). + # @param serviceName the dbus-service-name (string), for example 'com.victronenergy.battery.ttyO1' + # @param path the object-path, for example '/Dc/V' + # @param eventCallback function that you want to be called on a value change + # @param createSignal only set this to False if you use this function to one time read a value. When + # leaving it to True, make sure to also subscribe to the NameOwnerChanged signal + # elsewhere. See also note some 15 lines up. + def __init__(self, bus, serviceName, path, eventCallback=None, createsignal=True): + # TODO: is it necessary to store _serviceName and _path? Isn't it + # stored in the bus_getobjectsomewhere? + self._serviceName = serviceName + self._path = path + self._match = None + # TODO: _proxy is being used in settingsdevice.py, make a getter for that + self._proxy = bus.get_object(serviceName, path, introspect=False) + self.eventCallback = eventCallback + + assert eventCallback is None or createsignal == True + if createsignal: + self._match = self._proxy.connect_to_signal( + "PropertiesChanged", weak_functor(self._properties_changed_handler)) + self._roots[serviceName].add(self) + + # store the current value in _cachedvalue. When it doesn't exists set _cachedvalue to + # None, same as when a value is invalid + self._cachedvalue = None + try: + v = self._proxy.GetValue() + except dbus.exceptions.DBusException: + pass + else: + self._cachedvalue = unwrap_dbus_value(v) + + def __del__(self): + if self._match is not None: + self._match.remove() + self._match = None + self._proxy = None + + def _refreshcachedvalue(self): + self._cachedvalue = unwrap_dbus_value(self._proxy.GetValue()) + + ## Returns the path as a string, for example '/AC/L1/V' + @property + def path(self): + return self._path + + ## Returns the dbus service name as a string, for example com.victronenergy.vebus.ttyO1 + @property + def serviceName(self): + return self._serviceName + + ## Returns the value of the dbus-item. + # the type will be a dbus variant, for example dbus.Int32(0, variant_level=1) + # this is not a property to keep the name consistant with the com.victronenergy.busitem interface + # returns None when the property is invalid + def get_value(self): + return self._cachedvalue + + ## Writes a new value to the dbus-item + def set_value(self, newvalue): + r = self._proxy.SetValue(wrap_dbus_value(newvalue)) + + # instead of just saving the value, go to the dbus and get it. So we have the right type etc. + if r == 0: + self._refreshcachedvalue() + + return r + + ## Resets the item to its default value + def set_default(self): + self._proxy.SetDefault() + self._refreshcachedvalue() + + ## Returns the text representation of the value. + # For example when the value is an enum/int GetText might return the string + # belonging to that enum value. Another example, for a voltage, GetValue + # would return a float, 12.0Volt, and GetText could return 12 VDC. + # + # Note that this depends on how the dbus-producer has implemented this. + def get_text(self): + return self._proxy.GetText() + + ## Returns true of object path exists, and false if it doesn't + @property + def exists(self): + # TODO: do some real check instead of this crazy thing. + r = False + try: + r = self._proxy.GetValue() + r = True + except dbus.exceptions.DBusException: + pass + + return r + + ## callback for the trigger-event. + # @param eventCallback the event-callback-function. + @property + def eventCallback(self): + return self._eventCallback + + @eventCallback.setter + def eventCallback(self, eventCallback): + self._eventCallback = eventCallback + + ## Is called when the value of the imported bus-item changes. + # Stores the new value in our local cache, and calls the eventCallback, if set. + def _properties_changed_handler(self, changes): + if "Value" in changes: + changes['Value'] = unwrap_dbus_value(changes['Value']) + self._cachedvalue = changes['Value'] + if self._eventCallback: + # The reason behind this try/except is to prevent errors silently ending up the an error + # handler in the dbus code. + try: + self._eventCallback(self._serviceName, self._path, changes) + except: + traceback.print_exc() + os._exit(1) # sys.exit() is not used, since that also throws an exception + + +class VeDbusTreeExport(dbus.service.Object): + def __init__(self, bus, objectPath, service): + dbus.service.Object.__init__(self, bus, objectPath) + self._service = service + logging.debug("VeDbusTreeExport %s has been created" % objectPath) + + def __del__(self): + # self._get_path() will raise an exception when retrieved after the call to .remove_from_connection, + # so we need a copy. + path = self._get_path() + if path is None: + return + self.remove_from_connection() + logging.debug("VeDbusTreeExport %s has been removed" % path) + + def _get_path(self): + if len(self._locations) == 0: + return None + return self._locations[0][1] + + def _get_value_handler(self, path, get_text=False): + logging.debug("_get_value_handler called for %s" % path) + r = {} + px = path + if not px.endswith('/'): + px += '/' + for p, item in self._service._dbusobjects.items(): + if p.startswith(px): + v = item.GetText() if get_text else wrap_dbus_value(item.local_get_value()) + r[p[len(px):]] = v + logging.debug(r) + return r + + @dbus.service.method('com.victronenergy.BusItem', out_signature='v') + def GetValue(self): + value = self._get_value_handler(self._get_path()) + return dbus.Dictionary(value, signature=dbus.Signature('sv'), variant_level=1) + + @dbus.service.method('com.victronenergy.BusItem', out_signature='v') + def GetText(self): + return self._get_value_handler(self._get_path(), True) + + def local_get_value(self): + return self._get_value_handler(self.path) + +class VeDbusRootExport(VeDbusTreeExport): + @dbus.service.signal('com.victronenergy.BusItem', signature='a{sa{sv}}') + def ItemsChanged(self, changes): + pass + + @dbus.service.method('com.victronenergy.BusItem', out_signature='a{sa{sv}}') + def GetItems(self): + return { + path: { + 'Value': wrap_dbus_value(item.local_get_value()), + 'Text': item.GetText() } + for path, item in self._service._dbusobjects.items() + } + + +class VeDbusItemExport(dbus.service.Object): + ## Constructor of VeDbusItemExport + # + # Use this object to export (publish), values on the dbus + # Creates the dbus-object under the given dbus-service-name. + # @param bus The dbus object. + # @param objectPath The dbus-object-path. + # @param value Value to initialize ourselves with, defaults to None which means Invalid + # @param description String containing a description. Can be called over the dbus with GetDescription() + # @param writeable what would this do!? :). + # @param callback Function that will be called when someone else changes the value of this VeBusItem + # over the dbus. First parameter passed to callback will be our path, second the new + # value. This callback should return True to accept the change, False to reject it. + def __init__(self, bus, objectPath, value=None, description=None, writeable=False, + onchangecallback=None, gettextcallback=None, deletecallback=None, + valuetype=None): + dbus.service.Object.__init__(self, bus, objectPath) + self._onchangecallback = onchangecallback + self._gettextcallback = gettextcallback + self._value = value + self._description = description + self._writeable = writeable + self._deletecallback = deletecallback + self._type = valuetype + + # To force immediate deregistering of this dbus object, explicitly call __del__(). + def __del__(self): + # self._get_path() will raise an exception when retrieved after the + # call to .remove_from_connection, so we need a copy. + path = self._get_path() + if path == None: + return + if self._deletecallback is not None: + self._deletecallback(path) + self.remove_from_connection() + logging.debug("VeDbusItemExport %s has been removed" % path) + + def _get_path(self): + if len(self._locations) == 0: + return None + return self._locations[0][1] + + ## Sets the value. And in case the value is different from what it was, a signal + # will be emitted to the dbus. This function is to be used in the python code that + # is using this class to export values to the dbus. + # set value to None to indicate that it is Invalid + def local_set_value(self, newvalue): + changes = self._local_set_value(newvalue) + if changes is not None: + self.PropertiesChanged(changes) + + def _local_set_value(self, newvalue): + if self._value == newvalue: + return None + + self._value = newvalue + return { + 'Value': wrap_dbus_value(newvalue), + 'Text': self.GetText() + } + + def local_get_value(self): + return self._value + + # ==== ALL FUNCTIONS BELOW THIS LINE WILL BE CALLED BY OTHER PROCESSES OVER THE DBUS ==== + + ## Dbus exported method SetValue + # Function is called over the D-Bus by other process. It will first check (via callback) if new + # value is accepted. And it is, stores it and emits a changed-signal. + # @param value The new value. + # @return completion-code When successful a 0 is return, and when not a -1 is returned. + @dbus.service.method('com.victronenergy.BusItem', in_signature='v', out_signature='i') + def SetValue(self, newvalue): + if not self._writeable: + return 1 # NOT OK + + newvalue = unwrap_dbus_value(newvalue) + + # If value type is enforced, cast it. If the type can be coerced + # python will do it for us. This allows ints to become floats, + # or bools to become ints. Additionally also allow None, so that + # a path may be invalidated. + if self._type is not None and newvalue is not None: + try: + newvalue = self._type(newvalue) + except (ValueError, TypeError): + return 1 # NOT OK + + if newvalue == self._value: + return 0 # OK + + # call the callback given to us, and check if new value is OK. + if (self._onchangecallback is None or + (self._onchangecallback is not None and self._onchangecallback(self.__dbus_object_path__, newvalue))): + + self.local_set_value(newvalue) + return 0 # OK + + return 2 # NOT OK + + ## Dbus exported method GetDescription + # + # Returns the a description. + # @param language A language code (e.g. ISO 639-1 en-US). + # @param length Lenght of the language string. + # @return description + @dbus.service.method('com.victronenergy.BusItem', in_signature='si', out_signature='s') + def GetDescription(self, language, length): + return self._description if self._description is not None else 'No description given' + + ## Dbus exported method GetValue + # Returns the value. + # @return the value when valid, and otherwise an empty array + @dbus.service.method('com.victronenergy.BusItem', out_signature='v') + def GetValue(self): + return wrap_dbus_value(self._value) + + ## Dbus exported method GetText + # Returns the value as string of the dbus-object-path. + # @return text A text-value. '---' when local value is invalid + @dbus.service.method('com.victronenergy.BusItem', out_signature='s') + def GetText(self): + if self._value is None: + return '---' + + # Default conversion from dbus.Byte will get you a character (so 'T' instead of '84'), so we + # have to convert to int first. Note that if a dbus.Byte turns up here, it must have come from + # the application itself, as all data from the D-Bus should have been unwrapped by now. + if self._gettextcallback is None and type(self._value) == dbus.Byte: + return str(int(self._value)) + + if self._gettextcallback is None and self.__dbus_object_path__ == '/ProductId': + return "0x%X" % self._value + + if self._gettextcallback is None: + return str(self._value) + + return self._gettextcallback(self.__dbus_object_path__, self._value) + + ## The signal that indicates that the value has changed. + # Other processes connected to this BusItem object will have subscribed to the + # event when they want to track our state. + @dbus.service.signal('com.victronenergy.BusItem', signature='a{sv}') + def PropertiesChanged(self, changes): + pass + +## This class behaves like a regular reference to a class method (eg. self.foo), but keeps a weak reference +## to the object which method is to be called. +## Use this object to break circular references. +class weak_functor: + def __init__(self, f): + self._r = weakref.ref(f.__self__) + self._f = weakref.ref(f.__func__) + + def __call__(self, *args, **kargs): + r = self._r() + f = self._f() + if r == None or f == None: + return + f(r, *args, **kargs) diff --git a/velib_python/velib_python/latest/dbusmonitor.py b/velib_python/velib_python/latest/dbusmonitor.py new file mode 100644 index 0000000..fd25700 --- /dev/null +++ b/velib_python/velib_python/latest/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/latest/oldestVersion b/velib_python/velib_python/latest/oldestVersion new file mode 100644 index 0000000..bdea676 --- /dev/null +++ b/velib_python/velib_python/latest/oldestVersion @@ -0,0 +1 @@ +v3.40~39 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..cb95ba1 --- /dev/null +++ b/velib_python/velib_python/latest/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/velib_python/velib_python/v2.73/dbusmonitor.py b/velib_python/velib_python/v2.73/dbusmonitor.py new file mode 100644 index 0000000..fc8785d --- /dev/null +++ b/velib_python/velib_python/v2.73/dbusmonitor.py @@ -0,0 +1,535 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +## @package dbus_vrm +# This code takes care of the D-Bus interface (not all of below is implemented yet): +# - on startup it scans the dbus for services we know. For each known service found, it searches for +# objects/paths we know. Everything we find is stored in items{}, and an event is registered: if a +# value changes weĺl be notified and can pass that on to our owner. For example the vrmLogger. +# we know. +# - after startup, it continues to monitor the dbus: +# 1) when services are added we do the same check on that +# 2) when services are removed, we remove any items that we had that referred to that service +# 3) if an existing services adds paths we update ourselves as well: on init, we make a +# VeDbusItemImport for a non-, or not yet existing objectpaths as well1 +# +# Code is used by the vrmLogger, and also the pubsub code. Both are other modules in the dbus_vrm repo. + +from dbus.mainloop.glib import DBusGMainLoop +import gobject +from gobject import idle_add +import dbus +import dbus.service +import inspect +import logging +import argparse +import pprint +import traceback +import os +from collections import defaultdict +from functools import partial + +# our own packages +from vedbus import VeDbusItemExport, VeDbusItemImport +from ve_utils import exit_on_error, wrap_dbus_value, unwrap_dbus_value +notfound = object() # For lookups where None is a valid result + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) +class SystemBus(dbus.bus.BusConnection): + def __new__(cls): + return dbus.bus.BusConnection.__new__(cls, dbus.bus.BusConnection.TYPE_SYSTEM) + +class SessionBus(dbus.bus.BusConnection): + def __new__(cls): + return dbus.bus.BusConnection.__new__(cls, dbus.bus.BusConnection.TYPE_SESSION) + +class MonitoredValue(object): + def __init__(self, value, text, options): + super(MonitoredValue, self).__init__() + self.value = value + self.text = text + self.options = options + + # For legacy code, allow treating this as a tuple/list + def __iter__(self): + return iter((self.value, self.text, self.options)) + +class Service(object): + whentologoptions = ['configChange', 'onIntervalAlwaysAndOnEvent', + 'onIntervalOnlyWhenChanged', 'onIntervalAlways', 'never'] + def __init__(self, id, serviceName, deviceInstance): + super(Service, self).__init__() + self.id = id + self.name = serviceName + self.paths = {} + self._seen = set() + self.deviceInstance = deviceInstance + + self.configChange = [] + self.onIntervalAlwaysAndOnEvent = [] + self.onIntervalOnlyWhenChanged = [] + self.onIntervalAlways = [] + self.never = [] + + # For legacy code, attributes can still be accessed as if keys from a + # dictionary. + def __setitem__(self, key, value): + self.__dict__[key] = value + def __getitem__(self, key): + return self.__dict__[key] + + def set_seen(self, path): + self._seen.add(path) + + def seen(self, path): + return path in self._seen + + @property + def service_class(self): + return '.'.join(self.name.split('.')[:3]) + +class DbusMonitor(object): + ## Constructor + def __init__(self, dbusTree, valueChangedCallback=None, deviceAddedCallback=None, + deviceRemovedCallback=None, vebusDeviceInstance0=False): + # valueChangedCallback is the callback that we call when something has changed. + # def value_changed_on_dbus(dbusServiceName, dbusPath, options, changes, deviceInstance): + # in which changes is a tuple with GetText() and GetValue() + self.valueChangedCallback = valueChangedCallback + self.deviceAddedCallback = deviceAddedCallback + self.deviceRemovedCallback = deviceRemovedCallback + self.dbusTree = dbusTree + self.vebusDeviceInstance0 = vebusDeviceInstance0 + + # Lists all tracked services. Stores name, id, device instance, value per path, and whenToLog info + # indexed by service name (eg. com.victronenergy.settings). + self.servicesByName = {} + + # Same values as self.servicesByName, but indexed by service id (eg. :1.30) + self.servicesById = {} + + # Keep track of services by class to speed up calls to get_service_list + self.servicesByClass = defaultdict(list) + + # Keep track of any additional watches placed on items + self.serviceWatches = defaultdict(list) + + # For a PC, connect to the SessionBus + # For a CCGX, connect to the SystemBus + self.dbusConn = SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else SystemBus() + + # subscribe to NameOwnerChange for bus connect / disconnect events. + (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ \ + else dbus.SystemBus()).add_signal_receiver( + self.dbus_name_owner_changed, + signal_name='NameOwnerChanged') + + # Subscribe to PropertiesChanged for all services + self.dbusConn.add_signal_receiver(self.handler_value_changes, + dbus_interface='com.victronenergy.BusItem', + signal_name='PropertiesChanged', path_keyword='path', + sender_keyword='senderId') + + logger.info('===== Search on dbus for services that we will monitor starting... =====') + serviceNames = self.dbusConn.list_names() + for serviceName in serviceNames: + self.scan_dbus_service(serviceName) + + logger.info('===== Search on dbus for services that we will monitor finished =====') + + def dbus_name_owner_changed(self, name, oldowner, newowner): + if not name.startswith("com.victronenergy."): + return + + #decouple, and process in main loop + idle_add(exit_on_error, self._process_name_owner_changed, name, oldowner, newowner) + + def _process_name_owner_changed(self, name, oldowner, newowner): + if newowner != '': + # so we found some new service. Check if we can do something with it. + newdeviceadded = self.scan_dbus_service(name) + if newdeviceadded and self.deviceAddedCallback is not None: + self.deviceAddedCallback(name, self.get_device_instance(name)) + + elif name in self.servicesByName: + # it disappeared, we need to remove it. + logger.info("%s disappeared from the dbus. Removing it from our lists" % name) + service = self.servicesByName[name] + deviceInstance = service['deviceInstance'] + del self.servicesById[service.id] + del self.servicesByName[name] + for watch in self.serviceWatches[name]: + watch.remove() + del self.serviceWatches[name] + self.servicesByClass[service.service_class].remove(service) + if self.deviceRemovedCallback is not None: + self.deviceRemovedCallback(name, deviceInstance) + + def scan_dbus_service(self, serviceName): + try: + return self.scan_dbus_service_inner(serviceName) + except: + logger.error("Ignoring %s because of error while scanning:" % (serviceName)) + traceback.print_exc() + return False + + # Errors 'org.freedesktop.DBus.Error.ServiceUnknown' and + # 'org.freedesktop.DBus.Error.Disconnected' seem to happen when the service + # disappears while its being scanned. Which might happen, but is not really + # normal either, so letting them go into the logs. + + # Scans the given dbus service to see if it contains anything interesting for us. If it does, add + # it to our list of monitored D-Bus services. + def scan_dbus_service_inner(self, serviceName): + + # make it a normal string instead of dbus string + serviceName = str(serviceName) + + paths = self.dbusTree.get('.'.join(serviceName.split('.')[0:3]), None) + if paths is None: + logger.debug("Ignoring service %s, not in the tree" % serviceName) + return False + + logger.info("Found: %s, scanning and storing items" % serviceName) + serviceId = self.dbusConn.get_name_owner(serviceName) + + # we should never be notified to add a D-Bus service that we already have. If this assertion + # raises, check process_name_owner_changed, and D-Bus workings. + assert serviceName not in self.servicesByName + assert serviceId not in self.servicesById + + # for vebus.ttyO1, this is workaround, since VRM Portal expects the main vebus + # devices at instance 0. Not sure how to fix this yet. + if serviceName == 'com.victronenergy.vebus.ttyO1' and self.vebusDeviceInstance0: + di = 0 + elif serviceName == 'com.victronenergy.settings': + di = 0 + elif serviceName.startswith('com.victronenergy.vecan.'): + di = 0 + else: + try: + di = self.dbusConn.call_blocking(serviceName, + '/DeviceInstance', None, 'GetValue', '', []) + except dbus.exceptions.DBusException: + logger.info(" %s was skipped because it has no device instance" % serviceName) + return False # Skip it + else: + di = int(di) + + logger.info(" %s has device instance %s" % (serviceName, di)) + service = Service(serviceId, serviceName, di) + + # Let's try to fetch everything in one go + values = {} + texts = {} + try: + values.update(self.dbusConn.call_blocking(serviceName, '/', None, 'GetValue', '', [])) + texts.update(self.dbusConn.call_blocking(serviceName, '/', None, 'GetText', '', [])) + except: + pass + + for path, options in paths.iteritems(): + # path will be the D-Bus path: '/Ac/ActiveIn/L1/V' + # options will be a dictionary: {'code': 'V', 'whenToLog': 'onIntervalAlways'} + # check that the whenToLog setting is set to something we expect + assert options['whenToLog'] is None or options['whenToLog'] in Service.whentologoptions + + # Try to obtain the value we want from our bulk fetch. If we + # cannot find it there, do an individual query. + value = values.get(path[1:], notfound) + if value != notfound: + service.set_seen(path) + text = texts.get(path[1:], notfound) + if value is notfound or text is notfound: + try: + value = self.dbusConn.call_blocking(serviceName, path, None, 'GetValue', '', []) + service.set_seen(path) + text = self.dbusConn.call_blocking(serviceName, path, None, 'GetText', '', []) + except dbus.exceptions.DBusException as e: + if e.get_dbus_name() in ( + 'org.freedesktop.DBus.Error.ServiceUnknown', + 'org.freedesktop.DBus.Error.Disconnected'): + raise # This exception will be handled below + + # TODO org.freedesktop.DBus.Error.UnknownMethod really + # shouldn't happen but sometimes does. + logger.debug("%s %s does not exist (yet)" % (serviceName, path)) + value = None + text = None + + service.paths[path] = MonitoredValue(unwrap_dbus_value(value), unwrap_dbus_value(text), options) + + if options['whenToLog']: + service[options['whenToLog']].append(path) + + + logger.debug("Finished scanning and storing items for %s" % serviceName) + + # Adjust self at the end of the scan, so we don't have an incomplete set of + # data if an exception occurs during the scan. + self.servicesByName[serviceName] = service + self.servicesById[serviceId] = service + self.servicesByClass[service.service_class].append(service) + + return True + + def handler_value_changes(self, changes, path, senderId): + try: + service = self.servicesById[senderId] + a = service.paths[path] + except KeyError: + # Either senderId or path isn't there, which means + # it hasn't been scanned yet. + return + + # If this properyChange does not involve a value, our work is done. + if 'Value' not in changes: + return + + service.set_seen(path) + + # First update our store to the new value + changes['Value'] = unwrap_dbus_value(changes['Value']) + if a.value == changes['Value']: + return + + a.value = changes['Value'] + try: + a.text = changes['Text'] + except KeyError: + # Some services don't send Text with their PropertiesChanged events. + a.text = str(a.value) + + # And do the rest of the processing in on the mainloop + if self.valueChangedCallback is not None: + idle_add(exit_on_error, self._execute_value_changes, service.name, path, changes, a.options) + + def _execute_value_changes(self, serviceName, objectPath, changes, options): + # double check that the service still exists, as it might have + # disappeared between scheduling-for and executing this function. + if serviceName not in self.servicesByName: + return + + self.valueChangedCallback(serviceName, objectPath, + options, changes, self.get_device_instance(serviceName)) + + # Gets the value for a certain servicename and path + # The default_value is returned when: + # 1. When the service doesn't exist. + # 2. When the path asked for isn't being monitored. + # 3. When the path exists, but has dbus-invalid, ie an empty byte array. + # 4. When the path asked for is being monitored, but doesn't exist for that service. + def get_value(self, serviceName, objectPath, default_value=None): + service = self.servicesByName.get(serviceName, None) + if service is None: + return default_value + + value = service.paths.get(objectPath, None) + if value is None or value.value is None: + return default_value + + return value.value + + # returns if a dbus exists now, by doing a blocking dbus call. + # Typically seen will be sufficient and doesn't need access to the dbus. + def exists(self, serviceName, objectPath): + try: + self.dbusConn.call_blocking(serviceName, objectPath, None, 'GetValue', '', []) + return True + except dbus.exceptions.DBusException as e: + return False + + # Returns if there ever was a successful GetValue or valueChanged event. + # Unlike get_value this return True also if the actual value is invalid. + # + # Note: the path might no longer exists anymore, but that doesn't happen in + # practice. If a service really wants to reconfigure itself typically it should + # reconnect to the dbus which causes it to be rescanned and seen will be updated. + # If it is really needed to know if a path still exists, use exists. + def seen(self, serviceName, objectPath): + try: + return self.servicesByName[serviceName].seen(objectPath) + except KeyError: + return False + + # Sets the value for a certain servicename and path, returns the return value of the D-Bus SetValue + # method. If the underlying item does not exist (the service does not exist, or the objectPath was not + # registered) the function will return -1 + def set_value(self, serviceName, objectPath, value): + # Check if the D-Bus object referenced by serviceName and objectPath is registered. There is no + # necessity to do this, but it is in line with previous implementations which kept VeDbusItemImport + # objects for registers items only. + service = self.servicesByName.get(serviceName, None) + if service is None: + return -1 + if objectPath not in service.paths: + return -1 + # We do not catch D-Bus exceptions here, because the previous implementation did not do that either. + return self.dbusConn.call_blocking(serviceName, objectPath, + dbus_interface='com.victronenergy.BusItem', + method='SetValue', signature=None, + args=[wrap_dbus_value(value)]) + + # Similar to set_value, but operates asynchronously + def set_value_async(self, serviceName, objectPath, value, + reply_handler=None, error_handler=None): + service = self.servicesByName.get(serviceName, None) + if service is not None: + if objectPath in service.paths: + self.dbusConn.call_async(serviceName, objectPath, + dbus_interface='com.victronenergy.BusItem', + method='SetValue', signature=None, + args=[wrap_dbus_value(value)], + reply_handler=reply_handler, error_handler=error_handler) + return + + if error_handler is not None: + error_handler(TypeError('Service or path not found, ' + 'service=%s, path=%s' % (serviceName, objectPath))) + + # returns a dictionary, keys are the servicenames, value the instances + # optionally use the classfilter to get only a certain type of services, for + # example com.victronenergy.battery. + def get_service_list(self, classfilter=None): + if classfilter is None: + return { servicename: service.deviceInstance \ + for servicename, service in self.servicesByName.iteritems() } + + if classfilter not in self.servicesByClass: + return {} + + return { service.name: service.deviceInstance \ + for service in self.servicesByClass[classfilter] } + + def get_device_instance(self, serviceName): + return self.servicesByName[serviceName].deviceInstance + + # Parameter categoryfilter is to be a list, containing the categories you want (configChange, + # onIntervalAlways, etc). + # Returns a dictionary, keys are codes + instance, in VRM querystring format. For example vvt[0]. And + # values are the value. + def get_values(self, categoryfilter, converter=None): + + result = {} + + for serviceName in self.servicesByName: + result.update(self.get_values_for_service(categoryfilter, serviceName, converter)) + + return result + + # same as get_values above, but then for one service only + def get_values_for_service(self, categoryfilter, servicename, converter=None): + deviceInstance = self.get_device_instance(servicename) + result = {} + + service = self.servicesByName[servicename] + + for category in categoryfilter: + + for path in service[category]: + + value, text, options = service.paths[path] + + if value is not None: + + value = value if converter is None else converter.convert(path, options['code'], value, text) + + precision = options.get('precision') + if precision: + value = round(value, precision) + + result[options['code'] + "[" + str(deviceInstance) + "]"] = value + + return result + + def track_value(self, serviceName, objectPath, callback, *args, **kwargs): + """ A DbusMonitor can watch specific service/path combos for changes + so that it is not fully reliant on the global handler_value_changes + in this class. Additional watches are deleted automatically when + the service disappears from dbus. """ + self.serviceWatches[serviceName].append( + self.dbusConn.add_signal_receiver( + partial(callback, *args, **kwargs), + dbus_interface='com.victronenergy.BusItem', + signal_name='PropertiesChanged', + path=objectPath, bus_name=serviceName)) + + +# ====== ALL CODE BELOW THIS LINE IS PURELY FOR DEVELOPING THIS CLASS ====== + +# Example function that can be used as a starting point to use this code +def value_changed_on_dbus(dbusServiceName, dbusPath, dict, changes, deviceInstance): + logger.debug("0 ----------------") + logger.debug("1 %s%s changed" % (dbusServiceName, dbusPath)) + logger.debug("2 vrm dict : %s" % dict) + logger.debug("3 changes-text: %s" % changes['Text']) + logger.debug("4 changes-value: %s" % changes['Value']) + logger.debug("5 deviceInstance: %s" % deviceInstance) + logger.debug("6 - end") + + +def nameownerchange(a, b): + # used to find memory leaks in dbusmonitor and VeDbusItemImport + import gc + gc.collect() + objects = gc.get_objects() + print len([o for o in objects if type(o).__name__ == 'VeDbusItemImport']) + print len([o for o in objects if type(o).__name__ == 'SignalMatch']) + print len(objects) + + +def print_values(dbusmonitor): + a = dbusmonitor.get_value('wrongservice', '/DbusInvalid', default_value=1000) + b = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/NotInTheMonitorList', default_value=1000) + c = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/DbusInvalid', default_value=1000) + d = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/NonExistingButMonitored', default_value=1000) + + print "All should be 1000: Wrong Service: %s, NotInTheMonitorList: %s, DbusInvalid: %s, NonExistingButMonitored: %s" % (a, b, c, d) + return True + +# We have a mainloop, but that is just for developing this code. Normally above class & code is used from +# some other class, such as vrmLogger or the pubsub Implementation. +def main(): + # Init logging + logging.basicConfig(level=logging.DEBUG) + logger.info(__file__ + " is starting up") + + # Have a mainloop, so we can send/receive asynchronous calls to and from dbus + DBusGMainLoop(set_as_default=True) + + import os + import sys + sys.path.insert(1, os.path.join(os.path.dirname(__file__), '../../')) + + dummy = {'code': None, 'whenToLog': 'configChange', 'accessLevel': None} + monitorlist = {'com.victronenergy.dummyservice': { + '/Connected': dummy, + '/ProductName': dummy, + '/Mgmt/Connection': dummy, + '/Dc/0/Voltage': dummy, + '/Dc/0/Current': dummy, + '/Dc/0/Temperature': dummy, + '/Load/I': dummy, + '/FirmwareVersion': dummy, + '/DbusInvalid': dummy, + '/NonExistingButMonitored': dummy}} + + d = DbusMonitor(monitorlist, value_changed_on_dbus, + deviceAddedCallback=nameownerchange, deviceRemovedCallback=nameownerchange) + + # logger.info("==configchange values==") + # logger.info(pprint.pformat(d.get_values(['configChange']))) + + # logger.info("==onIntervalAlways and onIntervalOnlyWhenChanged==") + # logger.info(pprint.pformat(d.get_values(['onIntervalAlways', 'onIntervalAlwaysAndOnEvent']))) + + gobject.timeout_add(1000, print_values, d) + + # Start and run the mainloop + logger.info("Starting mainloop, responding on only events") + mainloop = gobject.MainLoop() + mainloop.run() + +if __name__ == "__main__": + main() diff --git a/velib_python/velib_python/v2.73/oldestVersion b/velib_python/velib_python/v2.73/oldestVersion new file mode 100644 index 0000000..c4da1d4 --- /dev/null +++ b/velib_python/velib_python/v2.73/oldestVersion @@ -0,0 +1 @@ +v2.71 diff --git a/velib_python/velib_python/v2.73/settingsdevice.py b/velib_python/velib_python/v2.73/settingsdevice.py new file mode 100644 index 0000000..b525375 --- /dev/null +++ b/velib_python/velib_python/v2.73/settingsdevice.py @@ -0,0 +1,115 @@ +import dbus +import logging +import time +from functools import partial + +# Local imports +from vedbus import VeDbusItemImport + +## Indexes for the setting dictonary. +PATH = 0 +VALUE = 1 +MINIMUM = 2 +MAXIMUM = 3 +SILENT = 4 + +## The Settings Device class. +# Used by python programs, such as the vrm-logger, to read and write settings they +# need to store on disk. And since these settings might be changed from a different +# source, such as the GUI, the program can pass an eventCallback that will be called +# as soon as some setting is changed. +# +# The settings are stored in flash via the com.victronenergy.settings service on dbus. +# See https://github.com/victronenergy/localsettings for more info. +# +# If there are settings in de supportSettings list which are not yet on the dbus, +# and therefore not yet in the xml file, they will be added through the dbus-addSetting +# interface of com.victronenergy.settings. +class SettingsDevice(object): + ## The constructor processes the tree of dbus-items. + # @param bus the system-dbus object + # @param name the dbus-service-name of the settings dbus service, 'com.victronenergy.settings' + # @param supportedSettings dictionary with all setting-names, and their defaultvalue, min, max and whether + # the setting is silent. The 'silent' entry is optional. If set to true, no changes in the setting will + # be logged by localsettings. + # @param eventCallback function that will be called on changes on any of these settings + # @param timeout Maximum interval to wait for localsettings. An exception is thrown at the end of the + # interval if the localsettings D-Bus service has not appeared yet. + def __init__(self, bus, supportedSettings, eventCallback, name='com.victronenergy.settings', timeout=0): + logging.debug("===== Settings device init starting... =====") + self._bus = bus + self._dbus_name = name + self._eventCallback = eventCallback + self._values = {} # stored the values, used to pass the old value along on a setting change + self._settings = {} + + count = 0 + while True: + if 'com.victronenergy.settings' in self._bus.list_names(): + break + if count == timeout: + raise Exception("The settings service com.victronenergy.settings does not exist!") + count += 1 + logging.info('waiting for settings') + time.sleep(1) + + # Add the items. + for setting, options in supportedSettings.items(): + silent = len(options) > SILENT and options[SILENT] + busitem = self.addSetting(options[PATH], options[VALUE], + options[MINIMUM], options[MAXIMUM], silent, callback=partial(self.handleChangedSetting, setting)) + self._settings[setting] = busitem + self._values[setting] = busitem.get_value() + + logging.debug("===== Settings device init finished =====") + + def addSetting(self, path, value, _min, _max, silent=False, callback=None): + busitem = VeDbusItemImport(self._bus, self._dbus_name, path, callback) + if busitem.exists and (value, _min, _max, silent) == busitem._proxy.GetAttributes(): + logging.debug("Setting %s found" % path) + else: + logging.info("Setting %s does not exist yet or must be adjusted" % path) + + # Prepare to add the setting. Most dbus types extend the python + # type so it is only necessary to additionally test for Int64. + if isinstance(value, (int, dbus.Int64)): + itemType = 'i' + elif isinstance(value, float): + itemType = 'f' + else: + itemType = 's' + + # Add the setting + # TODO, make an object that inherits VeDbusItemImport, and complete the D-Bus settingsitem interface + settings_item = VeDbusItemImport(self._bus, self._dbus_name, '/Settings', createsignal=False) + setting_path = path.replace('/Settings/', '', 1) + if silent: + settings_item._proxy.AddSilentSetting('', setting_path, value, itemType, _min, _max) + else: + settings_item._proxy.AddSetting('', setting_path, value, itemType, _min, _max) + + busitem = VeDbusItemImport(self._bus, self._dbus_name, path, callback) + + return busitem + + def handleChangedSetting(self, setting, servicename, path, changes): + oldvalue = self._values[setting] if setting in self._values else None + self._values[setting] = changes['Value'] + + if self._eventCallback is None: + return + + self._eventCallback(setting, oldvalue, changes['Value']) + + def setDefault(self, path): + item = VeDbusItemImport(self._bus, self._dbus_name, path, createsignal=False) + item.set_default() + + def __getitem__(self, setting): + return self._settings[setting].get_value() + + def __setitem__(self, setting, newvalue): + result = self._settings[setting].set_value(newvalue) + if result != 0: + # Trying to make some false change to our own settings? How dumb! + assert False diff --git a/velib_python/velib_python/v2.73/ve_utils.py b/velib_python/velib_python/v2.73/ve_utils.py new file mode 100644 index 0000000..c5cfb74 --- /dev/null +++ b/velib_python/velib_python/v2.73/ve_utils.py @@ -0,0 +1,253 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from traceback import print_exc +from os import _exit as os_exit +from os import statvfs +from subprocess import check_output, CalledProcessError +import logging +import dbus +logger = logging.getLogger(__name__) + +VEDBUS_INVALID = dbus.Array([], signature=dbus.Signature('i'), variant_level=1) + +# Use this function to make sure the code quits on an unexpected exception. Make sure to use it +# when using gobject.idle_add and also gobject.timeout_add. +# Without this, the code will just keep running, since gobject does not stop the mainloop on an +# exception. +# Example: gobject.idle_add(exit_on_error, myfunc, arg1, arg2) +def exit_on_error(func, *args, **kwargs): + try: + return func(*args, **kwargs) + except: + try: + print 'exit_on_error: there was an exception. Printing stacktrace will be tried and then exit' + print_exc() + except: + pass + + # sys.exit() is not used, since that throws an exception, which does not lead to a program + # halt when used in a dbus callback, see connection.py in the Python/Dbus libraries, line 230. + os_exit(1) + + +__vrm_portal_id = None +def get_vrm_portal_id(): + # For the CCGX, the definition of the VRM Portal ID is that it is the mac address of the onboard- + # ethernet port (eth0), stripped from its colons (:) and lower case. + + # nice coincidence is that this also works fine when running on your (linux) development computer. + + global __vrm_portal_id + + if __vrm_portal_id: + return __vrm_portal_id + + # First try the method that works if we don't have a data partition. This will fail + # when the current user is not root. + try: + __vrm_portal_id = check_output("/sbin/get-unique-id").strip() + return __vrm_portal_id + except (CalledProcessError, OSError): + pass + + # Attempt to get the id from /data/venus/unique-id where venus puts it + # on startup. + try: + __vrm_portal_id = open('/data/venus/unique-id').read().strip() + except IOError: + pass + else: + return __vrm_portal_id + + # Fall back to getting our id using a syscall. Assume we are on linux. + # Allow the user to override what interface is used using an environment + # variable. + import fcntl, socket, struct, os + + iface = os.environ.get('VRM_IFACE', 'eth0') + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', iface[:15])) + __vrm_portal_id = ''.join(['%02x' % ord(char) for char in info[18:24]]) + return __vrm_portal_id + + +# See VE.Can registers - public.docx for definition of this conversion +def convert_vreg_version_to_readable(version): + def str_to_arr(x, length): + a = [] + for i in range(0, len(x), length): + a.append(x[i:i+length]) + return a + + x = "%x" % version + x = x.upper() + + if len(x) == 5 or len(x) == 3 or len(x) == 1: + x = '0' + x + + a = str_to_arr(x, 2); + + # remove the first 00 if there are three bytes and it is 00 + if len(a) == 3 and a[0] == '00': + a.remove(0); + + # if we have two or three bytes now, and the first character is a 0, remove it + if len(a) >= 2 and a[0][0:1] == '0': + a[0] = a[0][1]; + + result = '' + for item in a: + result += ('.' if result != '' else '') + item + + + result = 'v' + result + + return result + + +def get_free_space(path): + result = -1 + + try: + s = statvfs(path) + result = s.f_frsize * s.f_bavail # Number of free bytes that ordinary users + except Exception, ex: + logger.info("Error while retrieving free space for path %s: %s" % (path, ex)) + + return result + + +def get_load_averages(): + c = read_file('/proc/loadavg') + return c.split(' ')[:3] + + +def _get_sysfs_machine_name(): + try: + with open('/sys/firmware/devicetree/base/model', 'r') as f: + return f.read().rstrip('\x00') + except IOError: + pass + + return None + +# Returns None if it cannot find a machine name. Otherwise returns the string +# containing the name +def get_machine_name(): + # First try calling the venus utility script + try: + return check_output("/usr/bin/product-name").strip() + except (CalledProcessError, OSError): + pass + + # Fall back to sysfs + name = _get_sysfs_machine_name() + if name is not None: + return name + + # Fall back to venus build machine name + try: + with open('/etc/venus/machine', 'r') as f: + return f.read().strip() + except IOError: + pass + + return None + + +def get_product_id(): + """ Find the machine ID and return it. """ + + # First try calling the venus utility script + try: + return check_output("/usr/bin/product-id").strip() + except (CalledProcessError, OSError): + pass + + # Fall back machine name mechanism + name = _get_sysfs_machine_name() + return { + 'Color Control GX': 'C001', + 'Venus GX': 'C002', + 'Octo GX': 'C006', + 'EasySolar-II': 'C007', + 'MultiPlus-II': 'C008' + }.get(name, 'C003') # C003 is Generic + + +# Returns False if it cannot open the file. Otherwise returns its rstripped contents +def read_file(path): + content = False + + try: + with open(path, 'r') as f: + content = f.read().rstrip() + except Exception, ex: + logger.debug("Error while reading %s: %s" % (path, ex)) + + return content + + +def wrap_dbus_value(value): + if value is None: + return VEDBUS_INVALID + if isinstance(value, float): + return dbus.Double(value, variant_level=1) + if isinstance(value, bool): + return dbus.Boolean(value, variant_level=1) + if isinstance(value, int): + try: + return dbus.Int32(value, variant_level=1) + except OverflowError: + return dbus.Int64(value, variant_level=1) + if isinstance(value, str): + return dbus.String(value, variant_level=1) + if isinstance(value, unicode): + return dbus.String(value, variant_level=1) + if isinstance(value, list): + if len(value) == 0: + # If the list is empty we cannot infer the type of the contents. So assume unsigned integer. + # A (signed) integer is dangerous, because an empty list of signed integers is used to encode + # an invalid value. + return dbus.Array([], signature=dbus.Signature('u'), variant_level=1) + return dbus.Array([wrap_dbus_value(x) for x in value], variant_level=1) + if isinstance(value, long): + return dbus.Int64(value, variant_level=1) + if isinstance(value, dict): + # Wrapping the keys of the dictionary causes D-Bus errors like: + # 'arguments to dbus_message_iter_open_container() were incorrect, + # assertion "(type == DBUS_TYPE_ARRAY && contained_signature && + # *contained_signature == DBUS_DICT_ENTRY_BEGIN_CHAR) || (contained_signature == NULL || + # _dbus_check_is_valid_signature (contained_signature))" failed in file ...' + return dbus.Dictionary({(k, wrap_dbus_value(v)) for k, v in value.items()}, variant_level=1) + return value + + +dbus_int_types = (dbus.Int32, dbus.UInt32, dbus.Byte, dbus.Int16, dbus.UInt16, dbus.UInt32, dbus.Int64, dbus.UInt64) + + +def unwrap_dbus_value(val): + """Converts D-Bus values back to the original type. For example if val is of type DBus.Double, + a float will be returned.""" + if isinstance(val, dbus_int_types): + return int(val) + if isinstance(val, dbus.Double): + return float(val) + if isinstance(val, dbus.Array): + v = [unwrap_dbus_value(x) for x in val] + return None if len(v) == 0 else v + if isinstance(val, (dbus.Signature, dbus.String)): + return unicode(val) + # Python has no byte type, so we convert to an integer. + if isinstance(val, dbus.Byte): + return int(val) + if isinstance(val, dbus.ByteArray): + return "".join([str(x) for x in val]) + if isinstance(val, (list, tuple)): + return [unwrap_dbus_value(x) for x in val] + if isinstance(val, (dbus.Dictionary, dict)): + # Do not unwrap the keys, see comment in wrap_dbus_value + return dict([(x, unwrap_dbus_value(y)) for x, y in val.items()]) + if isinstance(val, dbus.Boolean): + return bool(val) + return val diff --git a/velib_python/velib_python/v2.73/vedbus.py b/velib_python/velib_python/v2.73/vedbus.py new file mode 100644 index 0000000..7fbe55d --- /dev/null +++ b/velib_python/velib_python/v2.73/vedbus.py @@ -0,0 +1,501 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import dbus.service +import logging +import traceback +import os +import weakref +from ve_utils import wrap_dbus_value, unwrap_dbus_value + +# vedbus contains three classes: +# VeDbusItemImport -> use this to read data from the dbus, ie import +# VeDbusItemExport -> use this to export data to the dbus (one value) +# VeDbusService -> use that to create a service and export several values to the dbus + +# Code for VeDbusItemImport is copied from busitem.py and thereafter modified. +# All projects that used busitem.py need to migrate to this package. And some +# projects used to define there own equivalent of VeDbusItemExport. Better to +# use VeDbusItemExport, or even better the VeDbusService class that does it all for you. + +# TODOS +# 1 check for datatypes, it works now, but not sure if all is compliant with +# com.victronenergy.BusItem interface definition. See also the files in +# tests_and_examples. And see 'if type(v) == dbus.Byte:' on line 102. Perhaps +# something similar should also be done in VeDbusBusItemExport? +# 2 Shouldn't VeDbusBusItemExport inherit dbus.service.Object? +# 7 Make hard rules for services exporting data to the D-Bus, in order to make tracking +# changes possible. Does everybody first invalidate its data before leaving the bus? +# And what about before taking one object away from the bus, instead of taking the +# whole service offline? +# They should! And after taking one value away, do we need to know that someone left +# the bus? Or we just keep that value in invalidated for ever? Result is that we can't +# see the difference anymore between an invalidated value and a value that was first on +# the bus and later not anymore. See comments above VeDbusItemImport as well. +# 9 there are probably more todos in the code below. + +# Some thoughts with regards to the data types: +# +# Text from: http://dbus.freedesktop.org/doc/dbus-python/doc/tutorial.html#data-types +# --- +# Variants are represented by setting the variant_level keyword argument in the +# constructor of any D-Bus data type to a value greater than 0 (variant_level 1 +# means a variant containing some other data type, variant_level 2 means a variant +# containing a variant containing some other data type, and so on). If a non-variant +# is passed as an argument but introspection indicates that a variant is expected, +# it'll automatically be wrapped in a variant. +# --- +# +# Also the different dbus datatypes, such as dbus.Int32, and dbus.UInt32 are a subclass +# of Python int. dbus.String is a subclass of Python standard class unicode, etcetera +# +# So all together that explains why we don't need to explicitly convert back and forth +# between the dbus datatypes and the standard python datatypes. Note that all datatypes +# in python are objects. Even an int is an object. + +# The signature of a variant is 'v'. + +# Export ourselves as a D-Bus service. +class VeDbusService(object): + def __init__(self, servicename, bus=None): + # dict containing the VeDbusItemExport objects, with their path as the key. + self._dbusobjects = {} + self._dbusnodes = {} + + # dict containing the onchange callbacks, for each object. Object path is the key + self._onchangecallbacks = {} + + # Connect to session bus whenever present, else use the system bus + self._dbusconn = bus or (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else dbus.SystemBus()) + + # make the dbus connection available to outside, could make this a true property instead, but ach.. + self.dbusconn = self._dbusconn + + # Register ourselves on the dbus, trigger an error if already in use (do_not_queue) + self._dbusname = dbus.service.BusName(servicename, self._dbusconn, do_not_queue=True) + + # Add the root item that will return all items as a tree + self._dbusnodes['/'] = self._create_tree_export(self._dbusconn, '/', self._get_tree_dict) + + logging.info("registered ourselves on D-Bus as %s" % servicename) + + def _get_tree_dict(self, path, get_text=False): + logging.debug("_get_tree_dict called for %s" % path) + r = {} + px = path + if not px.endswith('/'): + px += '/' + for p, item in self._dbusobjects.items(): + if p.startswith(px): + v = item.GetText() if get_text else wrap_dbus_value(item.local_get_value()) + r[p[len(px):]] = v + logging.debug(r) + return r + + # To force immediate deregistering of this dbus service and all its object paths, explicitly + # call __del__(). + def __del__(self): + for node in self._dbusnodes.values(): + node.__del__() + self._dbusnodes.clear() + for item in self._dbusobjects.values(): + item.__del__() + self._dbusobjects.clear() + if self._dbusname: + self._dbusname.__del__() # Forces call to self._bus.release_name(self._name), see source code + self._dbusname = None + + # @param callbackonchange function that will be called when this value is changed. First parameter will + # be the path of the object, second the new value. This callback should return + # True to accept the change, False to reject it. + def add_path(self, path, value, description="", writeable=False, + onchangecallback=None, gettextcallback=None): + + if onchangecallback is not None: + self._onchangecallbacks[path] = onchangecallback + + item = VeDbusItemExport( + self._dbusconn, path, value, description, writeable, + self._value_changed, gettextcallback, deletecallback=self._item_deleted) + + spl = path.split('/') + for i in range(2, len(spl)): + subPath = '/'.join(spl[:i]) + if subPath not in self._dbusnodes and subPath not in self._dbusobjects: + self._dbusnodes[subPath] = self._create_tree_export(self._dbusconn, subPath, self._get_tree_dict) + self._dbusobjects[path] = item + logging.debug('added %s with start value %s. Writeable is %s' % (path, value, writeable)) + + # Add the mandatory paths, as per victron dbus api doc + def add_mandatory_paths(self, processname, processversion, connection, + deviceinstance, productid, productname, firmwareversion, hardwareversion, connected): + self.add_path('/Mgmt/ProcessName', processname) + self.add_path('/Mgmt/ProcessVersion', processversion) + self.add_path('/Mgmt/Connection', connection) + + # Create rest of the mandatory objects + self.add_path('/DeviceInstance', deviceinstance) + self.add_path('/ProductId', productid) + self.add_path('/ProductName', productname) + self.add_path('/FirmwareVersion', firmwareversion) + self.add_path('/HardwareVersion', hardwareversion) + self.add_path('/Connected', connected) + + def _create_tree_export(self, bus, objectPath, get_value_handler): + return VeDbusTreeExport(bus, objectPath, get_value_handler) + + # Callback function that is called from the VeDbusItemExport objects when a value changes. This function + # maps the change-request to the onchangecallback given to us for this specific path. + def _value_changed(self, path, newvalue): + if path not in self._onchangecallbacks: + return True + + return self._onchangecallbacks[path](path, newvalue) + + def _item_deleted(self, path): + self._dbusobjects.pop(path) + for np in self._dbusnodes.keys(): + if np != '/': + for ip in self._dbusobjects: + if ip.startswith(np + '/'): + break + else: + self._dbusnodes[np].__del__() + self._dbusnodes.pop(np) + + def __getitem__(self, path): + return self._dbusobjects[path].local_get_value() + + def __setitem__(self, path, newvalue): + self._dbusobjects[path].local_set_value(newvalue) + + def __delitem__(self, path): + self._dbusobjects[path].__del__() # Invalidates and then removes the object path + assert path not in self._dbusobjects + + def __contains__(self, path): + return path in self._dbusobjects + +""" +Importing basics: + - If when we power up, the D-Bus service does not exist, or it does exist and the path does not + yet exist, still subscribe to a signal: as soon as it comes online it will send a signal with its + initial value, which VeDbusItemImport will receive and use to update local cache. And, when set, + call the eventCallback. + - If when we power up, save it + - When using get_value, know that there is no difference between services (or object paths) that don't + exist and paths that are invalid (= empty array, see above). Both will return None. In case you do + really want to know ifa path exists or not, use the exists property. + - When a D-Bus service leaves the D-Bus, it will first invalidate all its values, and send signals + with that update, and only then leave the D-Bus. (or do we need to subscribe to the NameOwnerChanged- + signal!?!) To be discussed and make sure. Not really urgent, since all existing code that uses this + class already subscribes to the NameOwnerChanged signal, and subsequently removes instances of this + class. + +Read when using this class: +Note that when a service leaves that D-Bus without invalidating all its exported objects first, for +example because it is killed, VeDbusItemImport doesn't have a clue. So when using VeDbusItemImport, +make sure to also subscribe to the NamerOwnerChanged signal on bus-level. Or just use dbusmonitor, +because that takes care of all of that for you. +""" +class VeDbusItemImport(object): + ## Constructor + # @param bus the bus-object (SESSION or SYSTEM). + # @param serviceName the dbus-service-name (string), for example 'com.victronenergy.battery.ttyO1' + # @param path the object-path, for example '/Dc/V' + # @param eventCallback function that you want to be called on a value change + # @param createSignal only set this to False if you use this function to one time read a value. When + # leaving it to True, make sure to also subscribe to the NameOwnerChanged signal + # elsewhere. See also note some 15 lines up. + def __init__(self, bus, serviceName, path, eventCallback=None, createsignal=True): + # TODO: is it necessary to store _serviceName and _path? Isn't it + # stored in the bus_getobjectsomewhere? + self._serviceName = serviceName + self._path = path + self._match = None + # TODO: _proxy is being used in settingsdevice.py, make a getter for that + self._proxy = bus.get_object(serviceName, path, introspect=False) + self.eventCallback = eventCallback + + assert eventCallback is None or createsignal == True + if createsignal: + self._match = self._proxy.connect_to_signal( + "PropertiesChanged", weak_functor(self._properties_changed_handler)) + + # store the current value in _cachedvalue. When it doesn't exists set _cachedvalue to + # None, same as when a value is invalid + self._cachedvalue = None + try: + v = self._proxy.GetValue() + except dbus.exceptions.DBusException: + pass + else: + self._cachedvalue = unwrap_dbus_value(v) + + def __del__(self): + if self._match != None: + self._match.remove() + self._match = None + self._proxy = None + + def _refreshcachedvalue(self): + self._cachedvalue = unwrap_dbus_value(self._proxy.GetValue()) + + ## Returns the path as a string, for example '/AC/L1/V' + @property + def path(self): + return self._path + + ## Returns the dbus service name as a string, for example com.victronenergy.vebus.ttyO1 + @property + def serviceName(self): + return self._serviceName + + ## Returns the value of the dbus-item. + # the type will be a dbus variant, for example dbus.Int32(0, variant_level=1) + # this is not a property to keep the name consistant with the com.victronenergy.busitem interface + # returns None when the property is invalid + def get_value(self): + return self._cachedvalue + + ## Writes a new value to the dbus-item + def set_value(self, newvalue): + r = self._proxy.SetValue(wrap_dbus_value(newvalue)) + + # instead of just saving the value, go to the dbus and get it. So we have the right type etc. + if r == 0: + self._refreshcachedvalue() + + return r + + ## Resets the item to its default value + def set_default(self): + self._proxy.SetDefault() + self._refreshcachedvalue() + + ## Returns the text representation of the value. + # For example when the value is an enum/int GetText might return the string + # belonging to that enum value. Another example, for a voltage, GetValue + # would return a float, 12.0Volt, and GetText could return 12 VDC. + # + # Note that this depends on how the dbus-producer has implemented this. + def get_text(self): + return self._proxy.GetText() + + ## Returns true of object path exists, and false if it doesn't + @property + def exists(self): + # TODO: do some real check instead of this crazy thing. + r = False + try: + r = self._proxy.GetValue() + r = True + except dbus.exceptions.DBusException: + pass + + return r + + ## callback for the trigger-event. + # @param eventCallback the event-callback-function. + @property + def eventCallback(self): + return self._eventCallback + + @eventCallback.setter + def eventCallback(self, eventCallback): + self._eventCallback = eventCallback + + ## Is called when the value of the imported bus-item changes. + # Stores the new value in our local cache, and calls the eventCallback, if set. + def _properties_changed_handler(self, changes): + if "Value" in changes: + changes['Value'] = unwrap_dbus_value(changes['Value']) + self._cachedvalue = changes['Value'] + if self._eventCallback: + # The reason behind this try/except is to prevent errors silently ending up the an error + # handler in the dbus code. + try: + self._eventCallback(self._serviceName, self._path, changes) + except: + traceback.print_exc() + os._exit(1) # sys.exit() is not used, since that also throws an exception + + +class VeDbusTreeExport(dbus.service.Object): + def __init__(self, bus, objectPath, get_value_handler): + dbus.service.Object.__init__(self, bus, objectPath) + self._get_value_handler = get_value_handler + logging.debug("VeDbusTreeExport %s has been created" % objectPath) + + def __del__(self): + # self._get_path() will raise an exception when retrieved after the call to .remove_from_connection, + # so we need a copy. + path = self._get_path() + if path is None: + return + self.remove_from_connection() + logging.debug("VeDbusTreeExport %s has been removed" % path) + + def _get_path(self): + if len(self._locations) == 0: + return None + return self._locations[0][1] + + @dbus.service.method('com.victronenergy.BusItem', out_signature='v') + def GetValue(self): + value = self._get_value_handler(self._get_path()) + return dbus.Dictionary(value, signature=dbus.Signature('sv'), variant_level=1) + + @dbus.service.method('com.victronenergy.BusItem', out_signature='v') + def GetText(self): + return self._get_value_handler(self._get_path(), True) + + def local_get_value(self): + return self._get_value_handler(self.path) + + +class VeDbusItemExport(dbus.service.Object): + ## Constructor of VeDbusItemExport + # + # Use this object to export (publish), values on the dbus + # Creates the dbus-object under the given dbus-service-name. + # @param bus The dbus object. + # @param objectPath The dbus-object-path. + # @param value Value to initialize ourselves with, defaults to None which means Invalid + # @param description String containing a description. Can be called over the dbus with GetDescription() + # @param writeable what would this do!? :). + # @param callback Function that will be called when someone else changes the value of this VeBusItem + # over the dbus. First parameter passed to callback will be our path, second the new + # value. This callback should return True to accept the change, False to reject it. + def __init__(self, bus, objectPath, value=None, description=None, writeable=False, + onchangecallback=None, gettextcallback=None, deletecallback=None): + dbus.service.Object.__init__(self, bus, objectPath) + self._onchangecallback = onchangecallback + self._gettextcallback = gettextcallback + self._value = value + self._description = description + self._writeable = writeable + self._deletecallback = deletecallback + + # To force immediate deregistering of this dbus object, explicitly call __del__(). + def __del__(self): + # self._get_path() will raise an exception when retrieved after the + # call to .remove_from_connection, so we need a copy. + path = self._get_path() + if path == None: + return + if self._deletecallback is not None: + self._deletecallback(path) + self.local_set_value(None) + self.remove_from_connection() + logging.debug("VeDbusItemExport %s has been removed" % path) + + def _get_path(self): + if len(self._locations) == 0: + return None + return self._locations[0][1] + + ## Sets the value. And in case the value is different from what it was, a signal + # will be emitted to the dbus. This function is to be used in the python code that + # is using this class to export values to the dbus. + # set value to None to indicate that it is Invalid + def local_set_value(self, newvalue): + if self._value == newvalue: + return + + self._value = newvalue + + changes = {} + changes['Value'] = wrap_dbus_value(newvalue) + changes['Text'] = self.GetText() + self.PropertiesChanged(changes) + + def local_get_value(self): + return self._value + + # ==== ALL FUNCTIONS BELOW THIS LINE WILL BE CALLED BY OTHER PROCESSES OVER THE DBUS ==== + + ## Dbus exported method SetValue + # Function is called over the D-Bus by other process. It will first check (via callback) if new + # value is accepted. And it is, stores it and emits a changed-signal. + # @param value The new value. + # @return completion-code When successful a 0 is return, and when not a -1 is returned. + @dbus.service.method('com.victronenergy.BusItem', in_signature='v', out_signature='i') + def SetValue(self, newvalue): + if not self._writeable: + return 1 # NOT OK + + newvalue = unwrap_dbus_value(newvalue) + + if newvalue == self._value: + return 0 # OK + + # call the callback given to us, and check if new value is OK. + if (self._onchangecallback is None or + (self._onchangecallback is not None and self._onchangecallback(self.__dbus_object_path__, newvalue))): + + self.local_set_value(newvalue) + return 0 # OK + + return 2 # NOT OK + + ## Dbus exported method GetDescription + # + # Returns the a description. + # @param language A language code (e.g. ISO 639-1 en-US). + # @param length Lenght of the language string. + # @return description + @dbus.service.method('com.victronenergy.BusItem', in_signature='si', out_signature='s') + def GetDescription(self, language, length): + return self._description if self._description is not None else 'No description given' + + ## Dbus exported method GetValue + # Returns the value. + # @return the value when valid, and otherwise an empty array + @dbus.service.method('com.victronenergy.BusItem', out_signature='v') + def GetValue(self): + return wrap_dbus_value(self._value) + + ## Dbus exported method GetText + # Returns the value as string of the dbus-object-path. + # @return text A text-value. '---' when local value is invalid + @dbus.service.method('com.victronenergy.BusItem', out_signature='s') + def GetText(self): + if self._value is None: + return '---' + + # Default conversion from dbus.Byte will get you a character (so 'T' instead of '84'), so we + # have to convert to int first. Note that if a dbus.Byte turns up here, it must have come from + # the application itself, as all data from the D-Bus should have been unwrapped by now. + if self._gettextcallback is None and type(self._value) == dbus.Byte: + return str(int(self._value)) + + if self._gettextcallback is None and self.__dbus_object_path__ == '/ProductId': + return "0x%X" % self._value + + if self._gettextcallback is None: + return str(self._value) + + return self._gettextcallback(self.__dbus_object_path__, self._value) + + ## The signal that indicates that the value has changed. + # Other processes connected to this BusItem object will have subscribed to the + # event when they want to track our state. + @dbus.service.signal('com.victronenergy.BusItem', signature='a{sv}') + def PropertiesChanged(self, changes): + pass + +## This class behaves like a regular reference to a class method (eg. self.foo), but keeps a weak reference +## to the object which method is to be called. +## Use this object to break circular references. +class weak_functor: + def __init__(self, f): + self._r = weakref.ref(f.__self__) + self._f = weakref.ref(f.__func__) + + def __call__(self, *args, **kargs): + r = self._r() + f = self._f() + if r == None or f == None: + return + f(r, *args, **kargs) diff --git a/velib_python/velib_python/v2.94/dbusmonitor.py b/velib_python/velib_python/v2.94/dbusmonitor.py new file mode 100644 index 0000000..5f8e153 --- /dev/null +++ b/velib_python/velib_python/v2.94/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 +notfound = object() # For lookups where None is a valid result + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) +class SystemBus(dbus.bus.BusConnection): + def __new__(cls): + return dbus.bus.BusConnection.__new__(cls, dbus.bus.BusConnection.TYPE_SYSTEM) + +class SessionBus(dbus.bus.BusConnection): + def __new__(cls): + return dbus.bus.BusConnection.__new__(cls, dbus.bus.BusConnection.TYPE_SESSION) + +class MonitoredValue(object): + def __init__(self, value, text, options): + super(MonitoredValue, self).__init__() + self.value = value + self.text = text + self.options = options + + # For legacy code, allow treating this as a tuple/list + def __iter__(self): + return iter((self.value, self.text, self.options)) + +class Service(object): + whentologoptions = ['configChange', 'onIntervalAlwaysAndOnEvent', + 'onIntervalOnlyWhenChanged', 'onIntervalAlways', 'never'] + def __init__(self, id, serviceName, deviceInstance): + super(Service, self).__init__() + self.id = id + self.name = serviceName + self.paths = {} + self._seen = set() + self.deviceInstance = deviceInstance + + self.configChange = [] + self.onIntervalAlwaysAndOnEvent = [] + self.onIntervalOnlyWhenChanged = [] + self.onIntervalAlways = [] + self.never = [] + + # For legacy code, attributes can still be accessed as if keys from a + # dictionary. + def __setitem__(self, key, value): + self.__dict__[key] = value + def __getitem__(self, key): + return self.__dict__[key] + + def set_seen(self, path): + self._seen.add(path) + + def seen(self, path): + return path in self._seen + + @property + def service_class(self): + return '.'.join(self.name.split('.')[:3]) + +class DbusMonitor(object): + ## Constructor + def __init__(self, dbusTree, valueChangedCallback=None, deviceAddedCallback=None, + deviceRemovedCallback=None, vebusDeviceInstance0=False): + # valueChangedCallback is the callback that we call when something has changed. + # def value_changed_on_dbus(dbusServiceName, dbusPath, options, changes, deviceInstance): + # in which changes is a tuple with GetText() and GetValue() + self.valueChangedCallback = valueChangedCallback + self.deviceAddedCallback = deviceAddedCallback + self.deviceRemovedCallback = deviceRemovedCallback + self.dbusTree = dbusTree + self.vebusDeviceInstance0 = vebusDeviceInstance0 + + # Lists all tracked services. Stores name, id, device instance, value per path, and whenToLog info + # indexed by service name (eg. com.victronenergy.settings). + self.servicesByName = {} + + # Same values as self.servicesByName, but indexed by service id (eg. :1.30) + self.servicesById = {} + + # Keep track of services by class to speed up calls to get_service_list + self.servicesByClass = defaultdict(list) + + # Keep track of any additional watches placed on items + self.serviceWatches = defaultdict(list) + + # For a PC, connect to the SessionBus + # For a CCGX, connect to the SystemBus + self.dbusConn = SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else SystemBus() + + # subscribe to NameOwnerChange for bus connect / disconnect events. + (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ \ + else dbus.SystemBus()).add_signal_receiver( + self.dbus_name_owner_changed, + signal_name='NameOwnerChanged') + + # Subscribe to PropertiesChanged for all services + self.dbusConn.add_signal_receiver(self.handler_value_changes, + dbus_interface='com.victronenergy.BusItem', + signal_name='PropertiesChanged', path_keyword='path', + sender_keyword='senderId') + + # Subscribe to ItemsChanged for all services + self.dbusConn.add_signal_receiver(self.handler_item_changes, + dbus_interface='com.victronenergy.BusItem', + signal_name='ItemsChanged', path='/', + sender_keyword='senderId') + + logger.info('===== Search on dbus for services that we will monitor starting... =====') + serviceNames = self.dbusConn.list_names() + for serviceName in serviceNames: + self.scan_dbus_service(serviceName) + + logger.info('===== Search on dbus for services that we will monitor finished =====') + + def dbus_name_owner_changed(self, name, oldowner, newowner): + if not name.startswith("com.victronenergy."): + return + + #decouple, and process in main loop + GLib.idle_add(exit_on_error, self._process_name_owner_changed, name, oldowner, newowner) + + def _process_name_owner_changed(self, name, oldowner, newowner): + if newowner != '': + # so we found some new service. Check if we can do something with it. + newdeviceadded = self.scan_dbus_service(name) + if newdeviceadded and self.deviceAddedCallback is not None: + self.deviceAddedCallback(name, self.get_device_instance(name)) + + elif name in self.servicesByName: + # it disappeared, we need to remove it. + logger.info("%s disappeared from the dbus. Removing it from our lists" % name) + service = self.servicesByName[name] + deviceInstance = service['deviceInstance'] + del self.servicesById[service.id] + del self.servicesByName[name] + for watch in self.serviceWatches[name]: + watch.remove() + del self.serviceWatches[name] + self.servicesByClass[service.service_class].remove(service) + if self.deviceRemovedCallback is not None: + self.deviceRemovedCallback(name, deviceInstance) + + def scan_dbus_service(self, serviceName): + try: + return self.scan_dbus_service_inner(serviceName) + except: + logger.error("Ignoring %s because of error while scanning:" % (serviceName)) + traceback.print_exc() + return False + + # Errors 'org.freedesktop.DBus.Error.ServiceUnknown' and + # 'org.freedesktop.DBus.Error.Disconnected' seem to happen when the service + # disappears while its being scanned. Which might happen, but is not really + # normal either, so letting them go into the logs. + + # Scans the given dbus service to see if it contains anything interesting for us. If it does, add + # it to our list of monitored D-Bus services. + def scan_dbus_service_inner(self, serviceName): + + # make it a normal string instead of dbus string + serviceName = str(serviceName) + + paths = self.dbusTree.get('.'.join(serviceName.split('.')[0:3]), None) + if paths is None: + logger.debug("Ignoring service %s, not in the tree" % serviceName) + return False + + logger.info("Found: %s, scanning and storing items" % serviceName) + serviceId = self.dbusConn.get_name_owner(serviceName) + + # we should never be notified to add a D-Bus service that we already have. If this assertion + # raises, check process_name_owner_changed, and D-Bus workings. + assert serviceName not in self.servicesByName + assert serviceId not in self.servicesById + + # for vebus.ttyO1, this is workaround, since VRM Portal expects the main vebus + # devices at instance 0. Not sure how to fix this yet. + if serviceName == 'com.victronenergy.vebus.ttyO1' and self.vebusDeviceInstance0: + di = 0 + elif serviceName == 'com.victronenergy.settings': + di = 0 + elif serviceName.startswith('com.victronenergy.vecan.'): + di = 0 + else: + try: + di = self.dbusConn.call_blocking(serviceName, + '/DeviceInstance', None, 'GetValue', '', []) + except dbus.exceptions.DBusException: + logger.info(" %s was skipped because it has no device instance" % serviceName) + return False # Skip it + else: + di = int(di) + + logger.info(" %s has device instance %s" % (serviceName, di)) + service = Service(serviceId, serviceName, di) + + # Let's try to fetch everything in one go + values = {} + texts = {} + try: + values.update(self.dbusConn.call_blocking(serviceName, '/', None, 'GetValue', '', [])) + texts.update(self.dbusConn.call_blocking(serviceName, '/', None, 'GetText', '', [])) + except: + pass + + for path, options in paths.items(): + # path will be the D-Bus path: '/Ac/ActiveIn/L1/V' + # options will be a dictionary: {'code': 'V', 'whenToLog': 'onIntervalAlways'} + # check that the whenToLog setting is set to something we expect + assert options['whenToLog'] is None or options['whenToLog'] in Service.whentologoptions + + # Try to obtain the value we want from our bulk fetch. If we + # cannot find it there, do an individual query. + value = values.get(path[1:], notfound) + if value != notfound: + service.set_seen(path) + text = texts.get(path[1:], notfound) + if value is notfound or text is notfound: + try: + value = self.dbusConn.call_blocking(serviceName, path, None, 'GetValue', '', []) + service.set_seen(path) + text = self.dbusConn.call_blocking(serviceName, path, None, 'GetText', '', []) + except dbus.exceptions.DBusException as e: + if e.get_dbus_name() in ( + 'org.freedesktop.DBus.Error.ServiceUnknown', + 'org.freedesktop.DBus.Error.Disconnected'): + raise # This exception will be handled below + + # TODO org.freedesktop.DBus.Error.UnknownMethod really + # shouldn't happen but sometimes does. + logger.debug("%s %s does not exist (yet)" % (serviceName, path)) + value = None + text = None + + service.paths[path] = MonitoredValue(unwrap_dbus_value(value), unwrap_dbus_value(text), options) + + if options['whenToLog']: + service[options['whenToLog']].append(path) + + + logger.debug("Finished scanning and storing items for %s" % serviceName) + + # Adjust self at the end of the scan, so we don't have an incomplete set of + # data if an exception occurs during the scan. + self.servicesByName[serviceName] = service + self.servicesById[serviceId] = service + self.servicesByClass[service.service_class].append(service) + + return True + + def handler_item_changes(self, items, senderId): + if not isinstance(items, dict): + return + + try: + service = self.servicesById[senderId] + except KeyError: + # senderId isn't there, which means it hasn't been scanned yet. + return + + for path, changes in items.items(): + try: + v = unwrap_dbus_value(changes['Value']) + except (KeyError, TypeError): + continue + + try: + t = changes['Text'] + except KeyError: + t = str(v) + self._handler_value_changes(service, path, v, t) + + def handler_value_changes(self, changes, path, senderId): + # If this properyChange does not involve a value, our work is done. + if 'Value' not in changes: + return + + try: + service = self.servicesById[senderId] + except KeyError: + # senderId isn't there, which means it hasn't been scanned yet. + return + + v = unwrap_dbus_value(changes['Value']) + # Some services don't send Text with their PropertiesChanged events. + try: + t = changes['Text'] + except KeyError: + t = str(v) + self._handler_value_changes(service, path, v, t) + + def _handler_value_changes(self, service, path, value, text): + try: + a = service.paths[path] + except KeyError: + # path isn't there, which means it hasn't been scanned yet. + return + + service.set_seen(path) + + # First update our store to the new value + if a.value == value: + return + + a.value = value + a.text = text + + # And do the rest of the processing in on the mainloop + if self.valueChangedCallback is not None: + GLib.idle_add(exit_on_error, self._execute_value_changes, service.name, path, { + 'Value': value, 'Text': text}, a.options) + + def _execute_value_changes(self, serviceName, objectPath, changes, options): + # double check that the service still exists, as it might have + # disappeared between scheduling-for and executing this function. + if serviceName not in self.servicesByName: + return + + self.valueChangedCallback(serviceName, objectPath, + options, changes, self.get_device_instance(serviceName)) + + # Gets the value for a certain servicename and path + # The default_value is returned when: + # 1. When the service doesn't exist. + # 2. When the path asked for isn't being monitored. + # 3. When the path exists, but has dbus-invalid, ie an empty byte array. + # 4. When the path asked for is being monitored, but doesn't exist for that service. + def get_value(self, serviceName, objectPath, default_value=None): + service = self.servicesByName.get(serviceName, None) + if service is None: + return default_value + + value = service.paths.get(objectPath, None) + if value is None or value.value is None: + return default_value + + return value.value + + # returns if a dbus exists now, by doing a blocking dbus call. + # Typically seen will be sufficient and doesn't need access to the dbus. + def exists(self, serviceName, objectPath): + try: + self.dbusConn.call_blocking(serviceName, objectPath, None, 'GetValue', '', []) + return True + except dbus.exceptions.DBusException as e: + return False + + # Returns if there ever was a successful GetValue or valueChanged event. + # Unlike get_value this return True also if the actual value is invalid. + # + # Note: the path might no longer exists anymore, but that doesn't happen in + # practice. If a service really wants to reconfigure itself typically it should + # reconnect to the dbus which causes it to be rescanned and seen will be updated. + # If it is really needed to know if a path still exists, use exists. + def seen(self, serviceName, objectPath): + try: + return self.servicesByName[serviceName].seen(objectPath) + except KeyError: + return False + + # Sets the value for a certain servicename and path, returns the return value of the D-Bus SetValue + # method. If the underlying item does not exist (the service does not exist, or the objectPath was not + # registered) the function will return -1 + def set_value(self, serviceName, objectPath, value): + # Check if the D-Bus object referenced by serviceName and objectPath is registered. There is no + # necessity to do this, but it is in line with previous implementations which kept VeDbusItemImport + # objects for registers items only. + service = self.servicesByName.get(serviceName, None) + if service is None: + return -1 + if objectPath not in service.paths: + return -1 + # We do not catch D-Bus exceptions here, because the previous implementation did not do that either. + return self.dbusConn.call_blocking(serviceName, objectPath, + dbus_interface='com.victronenergy.BusItem', + method='SetValue', signature=None, + args=[wrap_dbus_value(value)]) + + # Similar to set_value, but operates asynchronously + def set_value_async(self, serviceName, objectPath, value, + reply_handler=None, error_handler=None): + service = self.servicesByName.get(serviceName, None) + if service is not None: + if objectPath in service.paths: + self.dbusConn.call_async(serviceName, objectPath, + dbus_interface='com.victronenergy.BusItem', + method='SetValue', signature=None, + args=[wrap_dbus_value(value)], + reply_handler=reply_handler, error_handler=error_handler) + return + + if error_handler is not None: + error_handler(TypeError('Service or path not found, ' + 'service=%s, path=%s' % (serviceName, objectPath))) + + # returns a dictionary, keys are the servicenames, value the instances + # optionally use the classfilter to get only a certain type of services, for + # example com.victronenergy.battery. + def get_service_list(self, classfilter=None): + if classfilter is None: + return { servicename: service.deviceInstance \ + for servicename, service in self.servicesByName.items() } + + if classfilter not in self.servicesByClass: + return {} + + return { service.name: service.deviceInstance \ + for service in self.servicesByClass[classfilter] } + + def get_device_instance(self, serviceName): + return self.servicesByName[serviceName].deviceInstance + + # Parameter categoryfilter is to be a list, containing the categories you want (configChange, + # onIntervalAlways, etc). + # Returns a dictionary, keys are codes + instance, in VRM querystring format. For example vvt[0]. And + # values are the value. + def get_values(self, categoryfilter, converter=None): + + result = {} + + for serviceName in self.servicesByName: + result.update(self.get_values_for_service(categoryfilter, serviceName, converter)) + + return result + + # same as get_values above, but then for one service only + def get_values_for_service(self, categoryfilter, servicename, converter=None): + deviceInstance = self.get_device_instance(servicename) + result = {} + + service = self.servicesByName[servicename] + + for category in categoryfilter: + + for path in service[category]: + + value, text, options = service.paths[path] + + if value is not None: + + value = value if converter is None else converter.convert(path, options['code'], value, text) + + precision = options.get('precision') + if precision: + value = round(value, precision) + + result[options['code'] + "[" + str(deviceInstance) + "]"] = value + + return result + + def track_value(self, serviceName, objectPath, callback, *args, **kwargs): + """ A DbusMonitor can watch specific service/path combos for changes + so that it is not fully reliant on the global handler_value_changes + in this class. Additional watches are deleted automatically when + the service disappears from dbus. """ + cb = partial(callback, *args, **kwargs) + + def root_tracker(items): + # Check if objectPath in dict + try: + v = items[objectPath] + _v = unwrap_dbus_value(v['Value']) + except (KeyError, TypeError): + return # not in this dict + + try: + t = v['Text'] + except KeyError: + cb({'Value': _v }) + else: + cb({'Value': _v, 'Text': t}) + + # Track changes on the path, and also on root + self.serviceWatches[serviceName].extend(( + self.dbusConn.add_signal_receiver(cb, + dbus_interface='com.victronenergy.BusItem', + signal_name='PropertiesChanged', + path=objectPath, bus_name=serviceName), + self.dbusConn.add_signal_receiver(root_tracker, + dbus_interface='com.victronenergy.BusItem', + signal_name='ItemsChanged', + path="/", bus_name=serviceName), + )) + + +# ====== ALL CODE BELOW THIS LINE IS PURELY FOR DEVELOPING THIS CLASS ====== + +# Example function that can be used as a starting point to use this code +def value_changed_on_dbus(dbusServiceName, dbusPath, dict, changes, deviceInstance): + logger.debug("0 ----------------") + logger.debug("1 %s%s changed" % (dbusServiceName, dbusPath)) + logger.debug("2 vrm dict : %s" % dict) + logger.debug("3 changes-text: %s" % changes['Text']) + logger.debug("4 changes-value: %s" % changes['Value']) + logger.debug("5 deviceInstance: %s" % deviceInstance) + logger.debug("6 - end") + + +def nameownerchange(a, b): + # used to find memory leaks in dbusmonitor and VeDbusItemImport + import gc + gc.collect() + objects = gc.get_objects() + print (len([o for o in objects if type(o).__name__ == 'VeDbusItemImport'])) + print (len([o for o in objects if type(o).__name__ == 'SignalMatch'])) + print (len(objects)) + + +def print_values(dbusmonitor): + a = dbusmonitor.get_value('wrongservice', '/DbusInvalid', default_value=1000) + b = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/NotInTheMonitorList', default_value=1000) + c = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/DbusInvalid', default_value=1000) + d = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/NonExistingButMonitored', default_value=1000) + + print ("All should be 1000: Wrong Service: %s, NotInTheMonitorList: %s, DbusInvalid: %s, NonExistingButMonitored: %s" % (a, b, c, d)) + return True + +# We have a mainloop, but that is just for developing this code. Normally above class & code is used from +# some other class, such as vrmLogger or the pubsub Implementation. +def main(): + # Init logging + logging.basicConfig(level=logging.DEBUG) + logger.info(__file__ + " is starting up") + + # Have a mainloop, so we can send/receive asynchronous calls to and from dbus + DBusGMainLoop(set_as_default=True) + + import os + import sys + sys.path.insert(1, os.path.join(os.path.dirname(__file__), '../../')) + + dummy = {'code': None, 'whenToLog': 'configChange', 'accessLevel': None} + monitorlist = {'com.victronenergy.dummyservice': { + '/Connected': dummy, + '/ProductName': dummy, + '/Mgmt/Connection': dummy, + '/Dc/0/Voltage': dummy, + '/Dc/0/Current': dummy, + '/Dc/0/Temperature': dummy, + '/Load/I': dummy, + '/FirmwareVersion': dummy, + '/DbusInvalid': dummy, + '/NonExistingButMonitored': dummy}} + + d = DbusMonitor(monitorlist, value_changed_on_dbus, + deviceAddedCallback=nameownerchange, deviceRemovedCallback=nameownerchange) + + # logger.info("==configchange values==") + # logger.info(pprint.pformat(d.get_values(['configChange']))) + + # logger.info("==onIntervalAlways and onIntervalOnlyWhenChanged==") + # logger.info(pprint.pformat(d.get_values(['onIntervalAlways', 'onIntervalAlwaysAndOnEvent']))) + + GLib.timeout_add(1000, print_values, d) + + # Start and run the mainloop + logger.info("Starting mainloop, responding on only events") + mainloop = GLib.MainLoop() + mainloop.run() + +if __name__ == "__main__": + main() diff --git a/velib_python/velib_python/v2.94/oldestVersion b/velib_python/velib_python/v2.94/oldestVersion new file mode 100644 index 0000000..366edb3 --- /dev/null +++ b/velib_python/velib_python/v2.94/oldestVersion @@ -0,0 +1 @@ +v2.80 diff --git a/velib_python/velib_python/v2.94/settingsdevice.py b/velib_python/velib_python/v2.94/settingsdevice.py new file mode 100644 index 0000000..a207e8b --- /dev/null +++ b/velib_python/velib_python/v2.94/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/v2.94/ve_utils.py b/velib_python/velib_python/v2.94/ve_utils.py new file mode 100644 index 0000000..e8d847d --- /dev/null +++ b/velib_python/velib_python/v2.94/ve_utils.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import sys +from traceback import print_exc +from os import _exit as os_exit +from os import statvfs +from subprocess import check_output, CalledProcessError +import logging +import dbus +logger = logging.getLogger(__name__) + +VEDBUS_INVALID = dbus.Array([], signature=dbus.Signature('i'), variant_level=1) + +class NoVrmPortalIdError(Exception): + pass + +# Use this function to make sure the code quits on an unexpected exception. Make sure to use it +# when using GLib.idle_add and also GLib.timeout_add. +# Without this, the code will just keep running, since GLib does not stop the mainloop on an +# exception. +# Example: GLib.idle_add(exit_on_error, myfunc, arg1, arg2) +def exit_on_error(func, *args, **kwargs): + try: + return func(*args, **kwargs) + except: + try: + print ('exit_on_error: there was an exception. Printing stacktrace will be tried and then exit') + print_exc() + except: + pass + + # sys.exit() is not used, since that throws an exception, which does not lead to a program + # halt when used in a dbus callback, see connection.py in the Python/Dbus libraries, line 230. + os_exit(1) + + +__vrm_portal_id = None +def get_vrm_portal_id(): + # The original definition of the VRM Portal ID is that it is the mac + # address of the onboard- ethernet port (eth0), stripped from its colons + # (:) and lower case. This may however differ between platforms. On Venus + # the task is therefore deferred to /sbin/get-unique-id so that a + # platform specific method can be easily defined. + # + # If /sbin/get-unique-id does not exist, then use the ethernet address + # of eth0. This also handles the case where velib_python is used as a + # package install on a Raspberry Pi. + # + # On a Linux host where the network interface may not be eth0, you can set + # the VRM_IFACE environment variable to the correct name. + + global __vrm_portal_id + + if __vrm_portal_id: + return __vrm_portal_id + + portal_id = None + + # First try the method that works if we don't have a data partition. This + # will fail when the current user is not root. + try: + portal_id = check_output("/sbin/get-unique-id").decode("utf-8", "ignore").strip() + if not portal_id: + raise NoVrmPortalIdError("get-unique-id returned blank") + __vrm_portal_id = portal_id + return portal_id + except CalledProcessError: + # get-unique-id returned non-zero + raise NoVrmPortalIdError("get-unique-id returned non-zero") + except OSError: + # File doesn't exist, use fallback + pass + + # Fall back to getting our id using a syscall. Assume we are on linux. + # Allow the user to override what interface is used using an environment + # variable. + import fcntl, socket, struct, os + + iface = os.environ.get('VRM_IFACE', 'eth0').encode('ascii') + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', iface[:15])) + except IOError: + raise NoVrmPortalIdError("ioctl failed for eth0") + + __vrm_portal_id = info[18:24].hex() + return __vrm_portal_id + + +# See VE.Can registers - public.docx for definition of this conversion +def convert_vreg_version_to_readable(version): + def str_to_arr(x, length): + a = [] + for i in range(0, len(x), length): + a.append(x[i:i+length]) + return a + + x = "%x" % version + x = x.upper() + + if len(x) == 5 or len(x) == 3 or len(x) == 1: + x = '0' + x + + a = str_to_arr(x, 2); + + # remove the first 00 if there are three bytes and it is 00 + if len(a) == 3 and a[0] == '00': + a.remove(0); + + # if we have two or three bytes now, and the first character is a 0, remove it + if len(a) >= 2 and a[0][0:1] == '0': + a[0] = a[0][1]; + + result = '' + for item in a: + result += ('.' if result != '' else '') + item + + + result = 'v' + result + + return result + + +def get_free_space(path): + result = -1 + + try: + s = statvfs(path) + result = s.f_frsize * s.f_bavail # Number of free bytes that ordinary users + except Exception as ex: + logger.info("Error while retrieving free space for path %s: %s" % (path, ex)) + + return result + + +def get_load_averages(): + c = read_file('/proc/loadavg') + return c.split(' ')[:3] + + +def _get_sysfs_machine_name(): + try: + with open('/sys/firmware/devicetree/base/model', 'r') as f: + return f.read().rstrip('\x00') + except IOError: + pass + + return None + +# Returns None if it cannot find a machine name. Otherwise returns the string +# containing the name +def get_machine_name(): + # First try calling the venus utility script + try: + return check_output("/usr/bin/product-name").strip().decode('UTF-8') + except (CalledProcessError, OSError): + pass + + # Fall back to sysfs + name = _get_sysfs_machine_name() + if name is not None: + return name + + # Fall back to venus build machine name + try: + with open('/etc/venus/machine', 'r', encoding='UTF-8') as f: + return f.read().strip() + except IOError: + pass + + return None + + +def get_product_id(): + """ Find the machine ID and return it. """ + + # First try calling the venus utility script + try: + return check_output("/usr/bin/product-id").strip() + except (CalledProcessError, OSError): + pass + + # Fall back machine name mechanism + name = _get_sysfs_machine_name() + return { + 'Color Control GX': 'C001', + 'Venus GX': 'C002', + 'Octo GX': 'C006', + 'EasySolar-II': 'C007', + 'MultiPlus-II': 'C008' + }.get(name, 'C003') # C003 is Generic + + +# Returns False if it cannot open the file. Otherwise returns its rstripped contents +def read_file(path): + content = False + + try: + with open(path, 'r') as f: + content = f.read().rstrip() + except Exception as ex: + logger.debug("Error while reading %s: %s" % (path, ex)) + + return content + + +def wrap_dbus_value(value): + if value is None: + return VEDBUS_INVALID + if isinstance(value, float): + return dbus.Double(value, variant_level=1) + if isinstance(value, bool): + return dbus.Boolean(value, variant_level=1) + if isinstance(value, int): + try: + return dbus.Int32(value, variant_level=1) + except OverflowError: + return dbus.Int64(value, variant_level=1) + if isinstance(value, str): + return dbus.String(value, variant_level=1) + if isinstance(value, list): + if len(value) == 0: + # If the list is empty we cannot infer the type of the contents. So assume unsigned integer. + # A (signed) integer is dangerous, because an empty list of signed integers is used to encode + # an invalid value. + return dbus.Array([], signature=dbus.Signature('u'), variant_level=1) + return dbus.Array([wrap_dbus_value(x) for x in value], variant_level=1) + if isinstance(value, dict): + # Wrapping the keys of the dictionary causes D-Bus errors like: + # 'arguments to dbus_message_iter_open_container() were incorrect, + # assertion "(type == DBUS_TYPE_ARRAY && contained_signature && + # *contained_signature == DBUS_DICT_ENTRY_BEGIN_CHAR) || (contained_signature == NULL || + # _dbus_check_is_valid_signature (contained_signature))" failed in file ...' + return dbus.Dictionary({(k, wrap_dbus_value(v)) for k, v in value.items()}, variant_level=1) + return value + + +dbus_int_types = (dbus.Int32, dbus.UInt32, dbus.Byte, dbus.Int16, dbus.UInt16, dbus.UInt32, dbus.Int64, dbus.UInt64) + + +def unwrap_dbus_value(val): + """Converts D-Bus values back to the original type. For example if val is of type DBus.Double, + a float will be returned.""" + if isinstance(val, dbus_int_types): + return int(val) + if isinstance(val, dbus.Double): + return float(val) + if isinstance(val, dbus.Array): + v = [unwrap_dbus_value(x) for x in val] + return None if len(v) == 0 else v + if isinstance(val, (dbus.Signature, dbus.String)): + return str(val) + # Python has no byte type, so we convert to an integer. + if isinstance(val, dbus.Byte): + return int(val) + if isinstance(val, dbus.ByteArray): + return "".join([bytes(x) for x in val]) + if isinstance(val, (list, tuple)): + return [unwrap_dbus_value(x) for x in val] + if isinstance(val, (dbus.Dictionary, dict)): + # Do not unwrap the keys, see comment in wrap_dbus_value + return dict([(x, unwrap_dbus_value(y)) for x, y in val.items()]) + if isinstance(val, dbus.Boolean): + return bool(val) + return val diff --git a/velib_python/velib_python/v2.94/vedbus.py b/velib_python/velib_python/v2.94/vedbus.py new file mode 100644 index 0000000..d6dac60 --- /dev/null +++ b/velib_python/velib_python/v2.94/vedbus.py @@ -0,0 +1,599 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import dbus.service +import logging +import traceback +import os +import weakref +from collections import defaultdict +from ve_utils import wrap_dbus_value, unwrap_dbus_value + +# vedbus contains three classes: +# VeDbusItemImport -> use this to read data from the dbus, ie import +# VeDbusItemExport -> use this to export data to the dbus (one value) +# VeDbusService -> use that to create a service and export several values to the dbus + +# Code for VeDbusItemImport is copied from busitem.py and thereafter modified. +# All projects that used busitem.py need to migrate to this package. And some +# projects used to define there own equivalent of VeDbusItemExport. Better to +# use VeDbusItemExport, or even better the VeDbusService class that does it all for you. + +# TODOS +# 1 check for datatypes, it works now, but not sure if all is compliant with +# com.victronenergy.BusItem interface definition. See also the files in +# tests_and_examples. And see 'if type(v) == dbus.Byte:' on line 102. Perhaps +# something similar should also be done in VeDbusBusItemExport? +# 2 Shouldn't VeDbusBusItemExport inherit dbus.service.Object? +# 7 Make hard rules for services exporting data to the D-Bus, in order to make tracking +# changes possible. Does everybody first invalidate its data before leaving the bus? +# And what about before taking one object away from the bus, instead of taking the +# whole service offline? +# They should! And after taking one value away, do we need to know that someone left +# the bus? Or we just keep that value in invalidated for ever? Result is that we can't +# see the difference anymore between an invalidated value and a value that was first on +# the bus and later not anymore. See comments above VeDbusItemImport as well. +# 9 there are probably more todos in the code below. + +# Some thoughts with regards to the data types: +# +# Text from: http://dbus.freedesktop.org/doc/dbus-python/doc/tutorial.html#data-types +# --- +# Variants are represented by setting the variant_level keyword argument in the +# constructor of any D-Bus data type to a value greater than 0 (variant_level 1 +# means a variant containing some other data type, variant_level 2 means a variant +# containing a variant containing some other data type, and so on). If a non-variant +# is passed as an argument but introspection indicates that a variant is expected, +# it'll automatically be wrapped in a variant. +# --- +# +# Also the different dbus datatypes, such as dbus.Int32, and dbus.UInt32 are a subclass +# of Python int. dbus.String is a subclass of Python standard class unicode, etcetera +# +# So all together that explains why we don't need to explicitly convert back and forth +# between the dbus datatypes and the standard python datatypes. Note that all datatypes +# in python are objects. Even an int is an object. + +# The signature of a variant is 'v'. + +# Export ourselves as a D-Bus service. +class VeDbusService(object): + def __init__(self, servicename, bus=None): + # dict containing the VeDbusItemExport objects, with their path as the key. + self._dbusobjects = {} + self._dbusnodes = {} + self._ratelimiters = [] + + # dict containing the onchange callbacks, for each object. Object path is the key + self._onchangecallbacks = {} + + # Connect to session bus whenever present, else use the system bus + self._dbusconn = bus or (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else dbus.SystemBus()) + + # make the dbus connection available to outside, could make this a true property instead, but ach.. + self.dbusconn = self._dbusconn + + # Register ourselves on the dbus, trigger an error if already in use (do_not_queue) + self._dbusname = dbus.service.BusName(servicename, self._dbusconn, do_not_queue=True) + + # Add the root item that will return all items as a tree + self._dbusnodes['/'] = VeDbusRootExport(self._dbusconn, '/', self) + + logging.info("registered ourselves on D-Bus as %s" % servicename) + + # To force immediate deregistering of this dbus service and all its object paths, explicitly + # call __del__(). + def __del__(self): + for node in list(self._dbusnodes.values()): + node.__del__() + self._dbusnodes.clear() + for item in list(self._dbusobjects.values()): + item.__del__() + self._dbusobjects.clear() + if self._dbusname: + self._dbusname.__del__() # Forces call to self._bus.release_name(self._name), see source code + self._dbusname = None + + # @param callbackonchange function that will be called when this value is changed. First parameter will + # be the path of the object, second the new value. This callback should return + # True to accept the change, False to reject it. + def add_path(self, path, value, description="", writeable=False, + onchangecallback=None, gettextcallback=None): + + if onchangecallback is not None: + self._onchangecallbacks[path] = onchangecallback + + item = VeDbusItemExport( + self._dbusconn, path, value, description, writeable, + self._value_changed, gettextcallback, deletecallback=self._item_deleted) + + spl = path.split('/') + for i in range(2, len(spl)): + subPath = '/'.join(spl[:i]) + if subPath not in self._dbusnodes and subPath not in self._dbusobjects: + self._dbusnodes[subPath] = VeDbusTreeExport(self._dbusconn, subPath, self) + self._dbusobjects[path] = item + logging.debug('added %s with start value %s. Writeable is %s' % (path, value, writeable)) + + # Add the mandatory paths, as per victron dbus api doc + def add_mandatory_paths(self, processname, processversion, connection, + deviceinstance, productid, productname, firmwareversion, hardwareversion, connected): + self.add_path('/Mgmt/ProcessName', processname) + self.add_path('/Mgmt/ProcessVersion', processversion) + self.add_path('/Mgmt/Connection', connection) + + # Create rest of the mandatory objects + self.add_path('/DeviceInstance', deviceinstance) + self.add_path('/ProductId', productid) + self.add_path('/ProductName', productname) + self.add_path('/FirmwareVersion', firmwareversion) + self.add_path('/HardwareVersion', hardwareversion) + self.add_path('/Connected', connected) + + # Callback function that is called from the VeDbusItemExport objects when a value changes. This function + # maps the change-request to the onchangecallback given to us for this specific path. + def _value_changed(self, path, newvalue): + if path not in self._onchangecallbacks: + return True + + return self._onchangecallbacks[path](path, newvalue) + + def _item_deleted(self, path): + self._dbusobjects.pop(path) + for np in list(self._dbusnodes.keys()): + if np != '/': + for ip in self._dbusobjects: + if ip.startswith(np + '/'): + break + else: + self._dbusnodes[np].__del__() + self._dbusnodes.pop(np) + + def __getitem__(self, path): + return self._dbusobjects[path].local_get_value() + + def __setitem__(self, path, newvalue): + self._dbusobjects[path].local_set_value(newvalue) + + def __delitem__(self, path): + self._dbusobjects[path].__del__() # Invalidates and then removes the object path + assert path not in self._dbusobjects + + def __contains__(self, path): + return path in self._dbusobjects + + def __enter__(self): + l = ServiceContext(self) + self._ratelimiters.append(l) + return l + + def __exit__(self, *exc): + # pop off the top one and flush it. If with statements are nested + # then each exit flushes its own part. + if self._ratelimiters: + self._ratelimiters.pop().flush() + +class ServiceContext(object): + def __init__(self, parent): + self.parent = parent + self.changes = {} + + def __getitem__(self, path): + return self.parent[path] + + def __setitem__(self, path, newvalue): + c = self.parent._dbusobjects[path]._local_set_value(newvalue) + if c is not None: + self.changes[path] = c + + def flush(self): + if self.changes: + self.parent._dbusnodes['/'].ItemsChanged(self.changes) + +class TrackerDict(defaultdict): + """ Same as defaultdict, but passes the key to default_factory. """ + def __missing__(self, key): + self[key] = x = self.default_factory(key) + return x + +class VeDbusRootTracker(object): + """ This tracks the root of a dbus path and listens for PropertiesChanged + signals. When a signal arrives, parse it and unpack the key/value changes + into traditional events, then pass it to the original eventCallback + method. """ + def __init__(self, bus, serviceName): + self.importers = defaultdict(weakref.WeakSet) + self.serviceName = serviceName + self._match = bus.get_object(serviceName, '/', introspect=False).connect_to_signal( + "ItemsChanged", weak_functor(self._items_changed_handler)) + + def __del__(self): + self._match.remove() + self._match = None + + def add(self, i): + self.importers[i.path].add(i) + + def _items_changed_handler(self, items): + if not isinstance(items, dict): + return + + for path, changes in items.items(): + try: + v = changes['Value'] + except KeyError: + continue + + try: + t = changes['Text'] + except KeyError: + t = str(unwrap_dbus_value(v)) + + for i in self.importers.get(path, ()): + i._properties_changed_handler({'Value': v, 'Text': t}) + +""" +Importing basics: + - If when we power up, the D-Bus service does not exist, or it does exist and the path does not + yet exist, still subscribe to a signal: as soon as it comes online it will send a signal with its + initial value, which VeDbusItemImport will receive and use to update local cache. And, when set, + call the eventCallback. + - If when we power up, save it + - When using get_value, know that there is no difference between services (or object paths) that don't + exist and paths that are invalid (= empty array, see above). Both will return None. In case you do + really want to know ifa path exists or not, use the exists property. + - When a D-Bus service leaves the D-Bus, it will first invalidate all its values, and send signals + with that update, and only then leave the D-Bus. (or do we need to subscribe to the NameOwnerChanged- + signal!?!) To be discussed and make sure. Not really urgent, since all existing code that uses this + class already subscribes to the NameOwnerChanged signal, and subsequently removes instances of this + class. + +Read when using this class: +Note that when a service leaves that D-Bus without invalidating all its exported objects first, for +example because it is killed, VeDbusItemImport doesn't have a clue. So when using VeDbusItemImport, +make sure to also subscribe to the NamerOwnerChanged signal on bus-level. Or just use dbusmonitor, +because that takes care of all of that for you. +""" +class VeDbusItemImport(object): + def __new__(cls, bus, serviceName, path, eventCallback=None, createsignal=True): + instance = object.__new__(cls) + + # If signal tracking should be done, also add to root tracker + if createsignal: + if "_roots" not in cls.__dict__: + cls._roots = TrackerDict(lambda k: VeDbusRootTracker(bus, k)) + + return instance + + ## Constructor + # @param bus the bus-object (SESSION or SYSTEM). + # @param serviceName the dbus-service-name (string), for example 'com.victronenergy.battery.ttyO1' + # @param path the object-path, for example '/Dc/V' + # @param eventCallback function that you want to be called on a value change + # @param createSignal only set this to False if you use this function to one time read a value. When + # leaving it to True, make sure to also subscribe to the NameOwnerChanged signal + # elsewhere. See also note some 15 lines up. + def __init__(self, bus, serviceName, path, eventCallback=None, createsignal=True): + # TODO: is it necessary to store _serviceName and _path? Isn't it + # stored in the bus_getobjectsomewhere? + self._serviceName = serviceName + self._path = path + self._match = None + # TODO: _proxy is being used in settingsdevice.py, make a getter for that + self._proxy = bus.get_object(serviceName, path, introspect=False) + self.eventCallback = eventCallback + + assert eventCallback is None or createsignal == True + if createsignal: + self._match = self._proxy.connect_to_signal( + "PropertiesChanged", weak_functor(self._properties_changed_handler)) + self._roots[serviceName].add(self) + + # store the current value in _cachedvalue. When it doesn't exists set _cachedvalue to + # None, same as when a value is invalid + self._cachedvalue = None + try: + v = self._proxy.GetValue() + except dbus.exceptions.DBusException: + pass + else: + self._cachedvalue = unwrap_dbus_value(v) + + def __del__(self): + if self._match is not None: + self._match.remove() + self._match = None + self._proxy = None + + def _refreshcachedvalue(self): + self._cachedvalue = unwrap_dbus_value(self._proxy.GetValue()) + + ## Returns the path as a string, for example '/AC/L1/V' + @property + def path(self): + return self._path + + ## Returns the dbus service name as a string, for example com.victronenergy.vebus.ttyO1 + @property + def serviceName(self): + return self._serviceName + + ## Returns the value of the dbus-item. + # the type will be a dbus variant, for example dbus.Int32(0, variant_level=1) + # this is not a property to keep the name consistant with the com.victronenergy.busitem interface + # returns None when the property is invalid + def get_value(self): + return self._cachedvalue + + ## Writes a new value to the dbus-item + def set_value(self, newvalue): + r = self._proxy.SetValue(wrap_dbus_value(newvalue)) + + # instead of just saving the value, go to the dbus and get it. So we have the right type etc. + if r == 0: + self._refreshcachedvalue() + + return r + + ## Resets the item to its default value + def set_default(self): + self._proxy.SetDefault() + self._refreshcachedvalue() + + ## Returns the text representation of the value. + # For example when the value is an enum/int GetText might return the string + # belonging to that enum value. Another example, for a voltage, GetValue + # would return a float, 12.0Volt, and GetText could return 12 VDC. + # + # Note that this depends on how the dbus-producer has implemented this. + def get_text(self): + return self._proxy.GetText() + + ## Returns true of object path exists, and false if it doesn't + @property + def exists(self): + # TODO: do some real check instead of this crazy thing. + r = False + try: + r = self._proxy.GetValue() + r = True + except dbus.exceptions.DBusException: + pass + + return r + + ## callback for the trigger-event. + # @param eventCallback the event-callback-function. + @property + def eventCallback(self): + return self._eventCallback + + @eventCallback.setter + def eventCallback(self, eventCallback): + self._eventCallback = eventCallback + + ## Is called when the value of the imported bus-item changes. + # Stores the new value in our local cache, and calls the eventCallback, if set. + def _properties_changed_handler(self, changes): + if "Value" in changes: + changes['Value'] = unwrap_dbus_value(changes['Value']) + self._cachedvalue = changes['Value'] + if self._eventCallback: + # The reason behind this try/except is to prevent errors silently ending up the an error + # handler in the dbus code. + try: + self._eventCallback(self._serviceName, self._path, changes) + except: + traceback.print_exc() + os._exit(1) # sys.exit() is not used, since that also throws an exception + + +class VeDbusTreeExport(dbus.service.Object): + def __init__(self, bus, objectPath, service): + dbus.service.Object.__init__(self, bus, objectPath) + self._service = service + logging.debug("VeDbusTreeExport %s has been created" % objectPath) + + def __del__(self): + # self._get_path() will raise an exception when retrieved after the call to .remove_from_connection, + # so we need a copy. + path = self._get_path() + if path is None: + return + self.remove_from_connection() + logging.debug("VeDbusTreeExport %s has been removed" % path) + + def _get_path(self): + if len(self._locations) == 0: + return None + return self._locations[0][1] + + def _get_value_handler(self, path, get_text=False): + logging.debug("_get_value_handler called for %s" % path) + r = {} + px = path + if not px.endswith('/'): + px += '/' + for p, item in self._service._dbusobjects.items(): + if p.startswith(px): + v = item.GetText() if get_text else wrap_dbus_value(item.local_get_value()) + r[p[len(px):]] = v + logging.debug(r) + return r + + @dbus.service.method('com.victronenergy.BusItem', out_signature='v') + def GetValue(self): + value = self._get_value_handler(self._get_path()) + return dbus.Dictionary(value, signature=dbus.Signature('sv'), variant_level=1) + + @dbus.service.method('com.victronenergy.BusItem', out_signature='v') + def GetText(self): + return self._get_value_handler(self._get_path(), True) + + def local_get_value(self): + return self._get_value_handler(self.path) + +class VeDbusRootExport(VeDbusTreeExport): + @dbus.service.signal('com.victronenergy.BusItem', signature='a{sa{sv}}') + def ItemsChanged(self, changes): + pass + + @dbus.service.method('com.victronenergy.BusItem', out_signature='a{sa{sv}}') + def GetItems(self): + return { + path: { + 'Value': wrap_dbus_value(item.local_get_value()), + 'Text': item.GetText() } + for path, item in self._service._dbusobjects.items() + } + + +class VeDbusItemExport(dbus.service.Object): + ## Constructor of VeDbusItemExport + # + # Use this object to export (publish), values on the dbus + # Creates the dbus-object under the given dbus-service-name. + # @param bus The dbus object. + # @param objectPath The dbus-object-path. + # @param value Value to initialize ourselves with, defaults to None which means Invalid + # @param description String containing a description. Can be called over the dbus with GetDescription() + # @param writeable what would this do!? :). + # @param callback Function that will be called when someone else changes the value of this VeBusItem + # over the dbus. First parameter passed to callback will be our path, second the new + # value. This callback should return True to accept the change, False to reject it. + def __init__(self, bus, objectPath, value=None, description=None, writeable=False, + onchangecallback=None, gettextcallback=None, deletecallback=None): + dbus.service.Object.__init__(self, bus, objectPath) + self._onchangecallback = onchangecallback + self._gettextcallback = gettextcallback + self._value = value + self._description = description + self._writeable = writeable + self._deletecallback = deletecallback + + # To force immediate deregistering of this dbus object, explicitly call __del__(). + def __del__(self): + # self._get_path() will raise an exception when retrieved after the + # call to .remove_from_connection, so we need a copy. + path = self._get_path() + if path == None: + return + if self._deletecallback is not None: + self._deletecallback(path) + self.local_set_value(None) + self.remove_from_connection() + logging.debug("VeDbusItemExport %s has been removed" % path) + + def _get_path(self): + if len(self._locations) == 0: + return None + return self._locations[0][1] + + ## Sets the value. And in case the value is different from what it was, a signal + # will be emitted to the dbus. This function is to be used in the python code that + # is using this class to export values to the dbus. + # set value to None to indicate that it is Invalid + def local_set_value(self, newvalue): + changes = self._local_set_value(newvalue) + if changes is not None: + self.PropertiesChanged(changes) + + def _local_set_value(self, newvalue): + if self._value == newvalue: + return None + + self._value = newvalue + return { + 'Value': wrap_dbus_value(newvalue), + 'Text': self.GetText() + } + + def local_get_value(self): + return self._value + + # ==== ALL FUNCTIONS BELOW THIS LINE WILL BE CALLED BY OTHER PROCESSES OVER THE DBUS ==== + + ## Dbus exported method SetValue + # Function is called over the D-Bus by other process. It will first check (via callback) if new + # value is accepted. And it is, stores it and emits a changed-signal. + # @param value The new value. + # @return completion-code When successful a 0 is return, and when not a -1 is returned. + @dbus.service.method('com.victronenergy.BusItem', in_signature='v', out_signature='i') + def SetValue(self, newvalue): + if not self._writeable: + return 1 # NOT OK + + newvalue = unwrap_dbus_value(newvalue) + + if newvalue == self._value: + return 0 # OK + + # call the callback given to us, and check if new value is OK. + if (self._onchangecallback is None or + (self._onchangecallback is not None and self._onchangecallback(self.__dbus_object_path__, newvalue))): + + self.local_set_value(newvalue) + return 0 # OK + + return 2 # NOT OK + + ## Dbus exported method GetDescription + # + # Returns the a description. + # @param language A language code (e.g. ISO 639-1 en-US). + # @param length Lenght of the language string. + # @return description + @dbus.service.method('com.victronenergy.BusItem', in_signature='si', out_signature='s') + def GetDescription(self, language, length): + return self._description if self._description is not None else 'No description given' + + ## Dbus exported method GetValue + # Returns the value. + # @return the value when valid, and otherwise an empty array + @dbus.service.method('com.victronenergy.BusItem', out_signature='v') + def GetValue(self): + return wrap_dbus_value(self._value) + + ## Dbus exported method GetText + # Returns the value as string of the dbus-object-path. + # @return text A text-value. '---' when local value is invalid + @dbus.service.method('com.victronenergy.BusItem', out_signature='s') + def GetText(self): + if self._value is None: + return '---' + + # Default conversion from dbus.Byte will get you a character (so 'T' instead of '84'), so we + # have to convert to int first. Note that if a dbus.Byte turns up here, it must have come from + # the application itself, as all data from the D-Bus should have been unwrapped by now. + if self._gettextcallback is None and type(self._value) == dbus.Byte: + return str(int(self._value)) + + if self._gettextcallback is None and self.__dbus_object_path__ == '/ProductId': + return "0x%X" % self._value + + if self._gettextcallback is None: + return str(self._value) + + return self._gettextcallback(self.__dbus_object_path__, self._value) + + ## The signal that indicates that the value has changed. + # Other processes connected to this BusItem object will have subscribed to the + # event when they want to track our state. + @dbus.service.signal('com.victronenergy.BusItem', signature='a{sv}') + def PropertiesChanged(self, changes): + pass + +## This class behaves like a regular reference to a class method (eg. self.foo), but keeps a weak reference +## to the object which method is to be called. +## Use this object to break circular references. +class weak_functor: + def __init__(self, f): + self._r = weakref.ref(f.__self__) + self._f = weakref.ref(f.__func__) + + def __call__(self, *args, **kargs): + r = self._r() + f = self._f() + if r == None or f == None: + return + f(r, *args, **kargs) diff --git a/velib_python/velib_python/v3.40~37/dbusmonitor.py b/velib_python/velib_python/v3.40~37/dbusmonitor.py new file mode 100644 index 0000000..cb2185d --- /dev/null +++ b/velib_python/velib_python/v3.40~37/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.40~37/oldestVersion b/velib_python/velib_python/v3.40~37/oldestVersion new file mode 100644 index 0000000..3f6c1a0 --- /dev/null +++ b/velib_python/velib_python/v3.40~37/oldestVersion @@ -0,0 +1 @@ +v3.00~32 diff --git a/velib_python/velib_python/v3.40~37/settingsdevice.py b/velib_python/velib_python/v3.40~37/settingsdevice.py new file mode 100644 index 0000000..a207e8b --- /dev/null +++ b/velib_python/velib_python/v3.40~37/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.40~37/ve_utils.py b/velib_python/velib_python/v3.40~37/ve_utils.py new file mode 100644 index 0000000..63a915b --- /dev/null +++ b/velib_python/velib_python/v3.40~37/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.40~37/vedbus.py b/velib_python/velib_python/v3.40~37/vedbus.py new file mode 100644 index 0000000..8c101ea --- /dev/null +++ b/velib_python/velib_python/v3.40~37/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/venus-data-UninstallPackages.tgz b/venus-data-UninstallPackages.tgz deleted file mode 100644 index 5d595f2dab2187d9042238a30b1ab43e008d2e74..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1141 zcmV-*1d96~iwFQx@`Ppp1MODtZ`(Ey?N{rsxX8v52a{z7`2j3&hGIpF0*zfXNxu|B zKuaf!uuK{xWv}b{zwby&vh1YkmleaX5J)VG$H#YfkB?T#&7hD8D_;D#F&qvr&d$Jo z(%AnTo}Q0RV03zZIvQP!MkgmQ9GzU8oV|eI59HL0QX8g-ijvYi6}QO|pDNORa{rOh zJ&0#q#51PzAn1b0xdxpmzA`{hE2a#{3>bv7f{PUHCT}M5uP0{w`h)G)n)4X5|4Ab=SXG8fuL5+t{*b;%8z6$k^pDIw$ZL|J9Bq*SEvOx*ku zQ_gGw3ONUs7{2D_b3k8SaWxL(HB&KtiFZkcHsCdZj>Dc0jgwL-6ebL4`vdp{^+X82 zUs7>V1fAs~$bCRjM?2Va5?Cb_JtekZE5eu3U`_{0?a1%~nSQvp|3 zE%>iN8$OJp2(0K07jy7!Eo&HvcG~C5OO)k{pu-P-irvE%k0c_F56yik23) zdE3OLQef3rz4^_ePD`^Y&A4|I%aw`OJTZ!51giPxn7X)1DLC>H)v@Cd`Ima2bBJV? zQA!9=CRIWoE~0PyadZT|Utv6kA@uuoHyTDRgQ*@HRbtT5n>&a35tZgr5F1zY-VR49 zH`^@W@XJb(K%qBz{q?XPV4?95+>E*R`@o@~HTDSd;jw8c6wjCXJB=250W=Scj#K)B{GW(3AKg1kqWUvqOs&!yIFE6xdJw$m4RA* zr=rI$M3Fik3%Zsm&v?QNMe5Kqa!RP}{h>|TW4}^JK*rn0+o}SYl1osf5VkFB&bSCt zMHrQ$W15N`LhL2&CY_*t7n+mS;J44`Zm}LY^w$4& z?28f-GAAC^c@sF)1>y=um+;Scx1T$NE3!gd$D*WK{p2d=PViIyRN3%uN_LeE8Vm;C zWbUK(_q6`pwauwS(Qb$n2prTW(V4cYe^x)ddQqg0T)biMZ~qc}9?#=>{NKjkB;w|F H01yBGe0V00 diff --git a/venus-data.tgz b/venus-data.tgz deleted file mode 100644 index 3c15fea404d2d1a72f10d4360bcbb1448adbf863..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 190649 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 AtN;K2 diff --git a/version b/version index 47889ea..01d66d2 100644 --- a/version +++ b/version @@ -1 +1 @@ -v8.6 +v8.7