[
  {
    "path": ".github/workflows/latest-tag.yml",
    "content": "name: Add latest tag to new release\non:\n  release:\n    types: [published]\n  workflow_dispatch:\n  \njobs:\n  run:\n    name: Run local action\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@master\n\n      - name: Run latest-tag\n        uses: EndBug/latest-tag@v1\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": "FileSets/PatchSource/PageSettings.qml",
    "content": "import QtQuick 1.1\nimport com.victron.velib 1.0\nimport net.connman 0.1\nimport \"utils.js\" as Utils\n\nMbPage {\n\ttitle: qsTr(\"Settings\")\n\tproperty string bindPrefix: \"com.victronenergy.settings\"\n\tproperty VBusItem relay0Item: VBusItem {bind: \"com.victronenergy.system/Relay/0/State\"}\n\tproperty bool hasRelay0: relay0Item.valid\n\n\tmodel: VisibleItemModel {\n\t\tMbSubMenu {\n\t\t\tid: generalItem\n\t\t\tdescription: qsTr(\"General\")\n\t\t\tsubpage: Component {\n\t\t\t\tPageSettingsGeneral {\n\t\t\t\t\ttitle: generalItem.description\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tdescription: qsTr(\"Firmware\")\n\t\t\tsubpage: Component {\n\t\t\t\tPageSettingsFirmware {\n\t\t\t\t\ttitle: qsTr(\"Firmware\")\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tdescription: qsTr(\"Date & Time\")\n\t\t\tsubpage: Component {\n\t\t\t\tPageTzInfo {\n\t\t\t\t\ttitle: qsTr(\"Date & Time\")\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tdescription: qsTr(\"Remote Console\")\n\t\t\tsubpage: Component { PageSettingsRemoteConsole {} }\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tid: systemSetupItem\n\t\t\tdescription: qsTr(\"System setup\")\n\t\t\tsubpage: Component {\n\t\t\t\tPageSettingsSystem {\n\t\t\t\t\ttitle: systemSetupItem.description\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tid: dvcc\n\t\t\tdescription: qsTr(\"DVCC\")\n\t\t\tsubpage: Component {\n\t\t\t\tPageSettingsDVCC {\n\t\t\t\t\ttitle: dvcc.description\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tid: displayItem\n\t\t\tdescription: qsTr(\"Display & language\")\n\t\t\tsubpage: Component {\n\t\t\t\tPageSettingsDisplay {\n\t\t\t\t\ttitle: displayItem.description\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tid: vrmLoggerItem\n\t\t\tdescription: qsTr(\"VRM online portal\")\n\t\t\tsubpage: Component {\n\t\t\t\tPageSettingsLogger {\n\t\t\t\t\ttitle: vrmLoggerItem.description\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tVBusItem {\n\t\t\t\tid: systemType\n\t\t\t\tbind: \"com.victronenergy.system/SystemType\"\n\t\t\t}\n\t\t\tdescription: systemType.value === \"Hub-4\" ? systemType.value : qsTr(\"ESS\")\n\t\t\tsubpage: Component { PageSettingsHub4 {} }\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tdescription: qsTr(\"Energy meters\")\n\t\t\tsubpage: Component { PageSettingsCGwacsOverview {} }\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tdescription: qsTr(\"PV inverters\")\n\t\t\tsubpage: Component { PageSettingsFronius {} }\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tshow: App.withQwacs\n\t\t\tdescription: qsTr(\"Wireless AC sensors\")\n\t\t\tsubpage: Component { PageSettingsQwacs {} }\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tdescription: qsTr(\"Modbus TCP/UDP devices\")\n\t\t\tsubpage: Component { PageSettingsModbus {} }\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tid: ethernetItem\n\t\t\tdescription: qsTr(\"Ethernet\")\n\t\t\tsubpage: Component { PageSettingsTcpIp { showLinkLocal: true } }\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tdescription: qsTr(\"Wi-Fi\")\n\t\t\tproperty VeQuickItem accessPoint: VeQuickItem { uid: \"dbus/com.victronenergy.platform/Services/AccessPoint/Enabled\" }\n\t\t\tsubpage: accessPoint.value !== undefined ? wifiWithAP : wifiWithoutAP\n\t\t\tComponent { id: wifiWithoutAP; PageSettingsWifi {} }\n\t\t\tComponent { id: wifiWithAP; PageSettingsWifiWithAccessPoint {} }\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tdescription: qsTr(\"GSM modem\")\n\t\t\tsubpage: Component { PageSettingsGsm {} }\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tdescription: qsTr(\"Bluetooth\")\n\t\t\tsubpage: Component { PageSettingsBluetooth {} }\n\t\t\tshow: Connman.technologyList.indexOf(\"bluetooth\") !== -1\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tdescription: qsTr(\"GPS\")\n\t\t\tsubpage: Component { PageSettingsGpsList {} }\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tdescription: qsTr(\"Generator start/stop\")\n\t\t\tsubpage: Component { PageRelayGenerator {} }\n\t\t\tshow: hasRelay0\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tdescription: qsTr(\"Tank pump\")\n\t\t\tsubpage: Component { PageSettingsTankPump {} }\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tdescription: qsTr(\"Relay\")\n\t\t\tsubpage: Component { PageSettingsRelay {} }\n\t\t\tshow: hasRelay0\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tdescription: qsTr(\"Services\")\n\t\t\tsubpage: Component { PageSettingsServices {} }\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tdescription: qsTr(\"I/O\")\n\t\t\tsubpage: ioSettings\n\t\t\tshow: ioSettings.haveSubMenus\n\t\t\tPageSettingsIo { id: ioSettings }\n\t\t}\n\n\t\t/*\n\t\tMbSubMenu {\n\t\t\tdescription: qsTr(\"Backup & Restore\")\n\t\t\tsubpage: Component { PageSettingsBackup {} }\n\t\t}\n\t\t*/\n\n\t\tMbSubMenu {\n\t\t\tdescription: qsTr(\"Venus OS Large features\")\n\t\t\tsubpage: Component { PageSettingsLarge {} }\n\t\t\tproperty VBusItem signalK: VBusItem { bind: \"com.victronenergy.platform/Services/SignalK/Enabled\" }\n\t\t\tproperty VBusItem nodeRed: VBusItem { bind: \"com.victronenergy.platform/Services/NodeRed/Mode\" }\n\t\t\tshow: signalK.valid || nodeRed.valid\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tdescription: \"Debug\"\n\t\t\tsubpage: Component { PageDebug {} }\n\t\t\tshowAccessLevel: User.AccessService\n\t\t}\n//////// added for PackageManager\n\t\tMbSubMenu\n\t\t{\n\t\t\tdescription: qsTr(\"Package manager\")\n\t\t\tsubpage: Component { PageSettingsPackageManager {} }\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "FileSets/PatchSource/PageSettings.qml.orig",
    "content": "import QtQuick 1.1\nimport com.victron.velib 1.0\nimport net.connman 0.1\nimport \"utils.js\" as Utils\n\nMbPage {\n\ttitle: qsTr(\"Settings\")\n\tproperty string bindPrefix: \"com.victronenergy.settings\"\n\tproperty VBusItem relay0Item: VBusItem {bind: \"com.victronenergy.system/Relay/0/State\"}\n\tproperty bool hasRelay0: relay0Item.valid\n\n\tmodel: VisibleItemModel {\n\t\tMbSubMenu {\n\t\t\tid: generalItem\n\t\t\tdescription: qsTr(\"General\")\n\t\t\tsubpage: Component {\n\t\t\t\tPageSettingsGeneral {\n\t\t\t\t\ttitle: generalItem.description\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tdescription: qsTr(\"Firmware\")\n\t\t\tsubpage: Component {\n\t\t\t\tPageSettingsFirmware {\n\t\t\t\t\ttitle: qsTr(\"Firmware\")\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tdescription: qsTr(\"Date & Time\")\n\t\t\tsubpage: Component {\n\t\t\t\tPageTzInfo {\n\t\t\t\t\ttitle: qsTr(\"Date & Time\")\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tdescription: qsTr(\"Remote Console\")\n\t\t\tsubpage: Component { PageSettingsRemoteConsole {} }\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tid: systemSetupItem\n\t\t\tdescription: qsTr(\"System setup\")\n\t\t\tsubpage: Component {\n\t\t\t\tPageSettingsSystem {\n\t\t\t\t\ttitle: systemSetupItem.description\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tid: dvcc\n\t\t\tdescription: qsTr(\"DVCC\")\n\t\t\tsubpage: Component {\n\t\t\t\tPageSettingsDVCC {\n\t\t\t\t\ttitle: dvcc.description\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tid: displayItem\n\t\t\tdescription: qsTr(\"Display & language\")\n\t\t\tsubpage: Component {\n\t\t\t\tPageSettingsDisplay {\n\t\t\t\t\ttitle: displayItem.description\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tid: vrmLoggerItem\n\t\t\tdescription: qsTr(\"VRM online portal\")\n\t\t\tsubpage: Component {\n\t\t\t\tPageSettingsLogger {\n\t\t\t\t\ttitle: vrmLoggerItem.description\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tVBusItem {\n\t\t\t\tid: systemType\n\t\t\t\tbind: \"com.victronenergy.system/SystemType\"\n\t\t\t}\n\t\t\tdescription: systemType.value === \"Hub-4\" ? systemType.value : qsTr(\"ESS\")\n\t\t\tsubpage: Component { PageSettingsHub4 {} }\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tdescription: qsTr(\"Energy meters\")\n\t\t\tsubpage: Component { PageSettingsCGwacsOverview {} }\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tdescription: qsTr(\"PV inverters\")\n\t\t\tsubpage: Component { PageSettingsFronius {} }\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tshow: App.withQwacs\n\t\t\tdescription: qsTr(\"Wireless AC sensors\")\n\t\t\tsubpage: Component { PageSettingsQwacs {} }\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tdescription: qsTr(\"Modbus TCP/UDP devices\")\n\t\t\tsubpage: Component { PageSettingsModbus {} }\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tid: ethernetItem\n\t\t\tdescription: qsTr(\"Ethernet\")\n\t\t\tsubpage: Component { PageSettingsTcpIp { showLinkLocal: true } }\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tdescription: qsTr(\"Wi-Fi\")\n\t\t\tproperty VeQuickItem accessPoint: VeQuickItem { uid: \"dbus/com.victronenergy.platform/Services/AccessPoint/Enabled\" }\n\t\t\tsubpage: accessPoint.value !== undefined ? wifiWithAP : wifiWithoutAP\n\t\t\tComponent { id: wifiWithoutAP; PageSettingsWifi {} }\n\t\t\tComponent { id: wifiWithAP; PageSettingsWifiWithAccessPoint {} }\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tdescription: qsTr(\"GSM modem\")\n\t\t\tsubpage: Component { PageSettingsGsm {} }\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tdescription: qsTr(\"Bluetooth\")\n\t\t\tsubpage: Component { PageSettingsBluetooth {} }\n\t\t\tshow: Connman.technologyList.indexOf(\"bluetooth\") !== -1\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tdescription: qsTr(\"GPS\")\n\t\t\tsubpage: Component { PageSettingsGpsList {} }\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tdescription: qsTr(\"Generator start/stop\")\n\t\t\tsubpage: Component { PageRelayGenerator {} }\n\t\t\tshow: hasRelay0\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tdescription: qsTr(\"Tank pump\")\n\t\t\tsubpage: Component { PageSettingsTankPump {} }\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tdescription: qsTr(\"Relay\")\n\t\t\tsubpage: Component { PageSettingsRelay {} }\n\t\t\tshow: hasRelay0\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tdescription: qsTr(\"Services\")\n\t\t\tsubpage: Component { PageSettingsServices {} }\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tdescription: qsTr(\"I/O\")\n\t\t\tsubpage: ioSettings\n\t\t\tshow: ioSettings.haveSubMenus\n\t\t\tPageSettingsIo { id: ioSettings }\n\t\t}\n\n\t\t/*\n\t\tMbSubMenu {\n\t\t\tdescription: qsTr(\"Backup & Restore\")\n\t\t\tsubpage: Component { PageSettingsBackup {} }\n\t\t}\n\t\t*/\n\n\t\tMbSubMenu {\n\t\t\tdescription: qsTr(\"Venus OS Large features\")\n\t\t\tsubpage: Component { PageSettingsLarge {} }\n\t\t\tproperty VBusItem signalK: VBusItem { bind: \"com.victronenergy.platform/Services/SignalK/Enabled\" }\n\t\t\tproperty VBusItem nodeRed: VBusItem { bind: \"com.victronenergy.platform/Services/NodeRed/Mode\" }\n\t\t\tshow: signalK.valid || nodeRed.valid\n\t\t}\n\n\t\tMbSubMenu {\n\t\t\tdescription: \"Debug\"\n\t\t\tsubpage: Component { PageDebug {} }\n\t\t\tshowAccessLevel: User.AccessService\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "FileSets/PatchSource/PageSettings.qml.patch",
    "content": "--- /Users/Kevin/GitHub/SetupHelper.copy/FileSets/PatchSource/PageSettings.qml.orig\t2024-05-15 13:06:53\n+++ /Users/Kevin/GitHub/SetupHelper.copy/FileSets/PatchSource/PageSettings.qml\t2025-01-24 22:39:59\n@@ -192,5 +192,11 @@\n \t\t\tsubpage: Component { PageDebug {} }\n \t\t\tshowAccessLevel: User.AccessService\n \t\t}\n+//////// added for PackageManager\n+\t\tMbSubMenu\n+\t\t{\n+\t\t\tdescription: qsTr(\"Package manager\")\n+\t\t\tsubpage: Component { PageSettingsPackageManager {} }\n+\t\t}\n \t}\n }\n"
  },
  {
    "path": "FileSets/VersionIndependent/MbDisplayDefaultPackage.qml",
    "content": "//////// new for PackageManager\n\nimport QtQuick 1.1\nimport com.victron.velib 1.0\nimport \"utils.js\" as Utils\n\nMbItem {\n\tid: root\n\n\tproperty int defaultIndex\n\tproperty string servicePrefix\n\n    property bool isCurrentItem: root.ListView.isCurrentItem\n\tproperty MbStyle style: MbStyle { isCurrentItem: root.ListView.isCurrentItem }\n\n    VBusItem { id: packageName; bind: getServiceBind (\"PackageName\") }\n\n\n\tonClicked: pageStack.push (\"/opt/victronenergy/gui/qml/PageSettingsPackageAdd.qml\", {defaultIndex: defaultIndex})\n\n\n\tfunction getServiceBind(param)\n\t{\n\t\treturn Utils.path(servicePrefix, \"/Default/\", defaultIndex, \"/\", param)\n\t}\n\n\n    MbRowSmall\n    {\n        description: \"\"\n\n        anchors.verticalCenter: parent.verticalCenter\n\t\tColumn\n\t\t{\n\t\t\twidth: root.width - gitHubUser.width - gitHubBranch.width - 20\n\t\t\tText // puts a bit of space above package name\n\t\t\t{\n\t\t\t\ttext: \" \"\n                font.pixelSize: 6\n\t\t\t}\n\t\t\tText\n\t\t\t{\n\t\t\t\ttext:packageName.valid ? packageName.value : \"\"\n\t\t\t\tcolor: isCurrentItem ? root.style.textColorSelected : root.style.textColor\n\t\t\t\tfont.pixelSize: 14\n\t\t\t\thorizontalAlignment: Text.AlignLeft\n\t\t\t}\n\t\t\tText\n\t\t\t{\n\t\t\t\ttext: \"\"\n\t\t\t\tfont.pixelSize: 10\n\t\t\t\thorizontalAlignment: Text.AlignLeft\n\t\t\t}\n\t\t}\n\t\tColumn\n\t\t{\n\t\t\tText // puts a bit of space above version boxes\n\t\t\t{\n\t\t\t\ttext: \" \"\n                font.pixelSize: 3\n\t\t\t}\n\t\t\tText\n\t\t\t{\n\t\t\t\ttext: \"GitHub User\"\n\t\t\t\tcolor: isCurrentItem ? root.style.textColorSelected : root.style.textColor\n                font.pixelSize: 10\n\t\t\t}\n\t\t\tMbTextBlock\n\t\t\t{\n\t\t\t\tid: gitHubUser\n\t\t\t\titem { bind: getServiceBind(\"GitHubUser\") }\n\t\t\t\theight: 20; width: 120\n\t\t\t}\n\t\t\tText // puts a bit of space below version boxes - only needed in one column\n\t\t\t{\n\t\t\t\ttext: \" \"\n                font.pixelSize: 6\n\t\t\t}\n        }\n\t\tColumn\n\t\t{\n\t\t\tText // puts a bit of space above version boxes\n\t\t\t{\n\t\t\t\ttext: \" \"\n                font.pixelSize: 3\n\t\t\t}\n\t\t\tText\n\t\t\t{\n\t\t\t\ttext: qsTr (\"GitHub Tag\")\n\t\t\t\tcolor: isCurrentItem ? root.style.textColorSelected : root.style.textColor\n                font.pixelSize: 10\n\t\t\t}\n\t\t\tMbTextBlock\n\t\t\t{\n\t\t\t\tid: gitHubBranch\n\t\t\t\titem { bind: getServiceBind(\"GitHubBranch\") }\n\t\t\t\theight: 20; width: 120\n\t\t\t}\n\t\t}\n    }\n}\n"
  },
  {
    "path": "FileSets/VersionIndependent/MbDisplayPackageVersion.qml",
    "content": "//////// new for PackageManager\n\nimport QtQuick 1.1\nimport com.victron.velib 1.0\nimport \"utils.js\" as Utils\n\nMbItem {\n\tid: root\n\n\tproperty int packageIndex\n\tproperty string servicePrefix\n\tproperty string settingsPrefix\n\n    property bool isCurrentItem: root.ListView.isCurrentItem\n\tproperty MbStyle style: MbStyle { isCurrentItem: root.ListView.isCurrentItem }\n\n\tVBusItem { id: packageName; bind: getSettingsBind (\"PackageName\") }\n    property VBusItem rebootNeededItem: VBusItem { bind: getServiceBind ( \"RebootNeeded\") }\n    property VBusItem guiRestartNeededItem: VBusItem { bind: getServiceBind ( \"GuiRestartNeeded\") }\n    property bool rebootNeeded: rebootNeededItem.valid && rebootNeededItem.value == 1\n    property bool guiRestartNeeded: guiRestartNeededItem.valid && guiRestartNeededItem.value == 1\n\n    VBusItem { id: incompatibleItem; bind: getServiceBind ( \"Incompatible\" ) }\n    property string incompatibleReason: incompatibleItem.valid ? incompatibleItem.value : \"\"\n    property bool compatible: incompatibleReason == \"\"\n    VBusItem { id: platformItem; bind: Utils.path(\"com.victronenergy.packageManager\", \"/Platform\" ) }\n    property string platform: platformItem.valid ? platformItem.value : \"???\"\n\n\t// version info may be in platform service or in vePlatform.version\n    VBusItem { id: osVersionItem; bind: Utils.path(\"com.victronenergy.platform\", \"/Firmware/Installed/Version\" ) }\n    property string osVersion: osVersionItem.valid ? osVersionItem.value : vePlatform.version\n\n\tonClicked: pageStack.push (\"/opt/victronenergy/gui/qml/PageSettingsPackageEdit.qml\", {newPackageIndex: packageIndex})\n\n\n\tfunction statusText ()\n\t{\n\t\tif (rebootNeeded)\n\t\t\treturn qsTr (\"REBOOT needed\")\n\t\tif (guiRestartNeeded)\n\t\t\treturn qsTr (\"GUI restart needed\")\n\t\telse if (incompatibleReason == 'PLATFORM')\n\t\t\treturn qsTr ( \"incompatible with \" + platform )\n\t\t// don't show warning incompatibilities here - they are shown in the editor menu\t\n\t\telse if (incompatibleReason != \"\"\n\t\t\t\t&& incompatibleReason.toLowerCase().indexOf (\"warning\") == -1 )\n\t\t\treturn incompatibleReason\n\t\telse\n\t\t\treturn \"\"\n\t}\n\n\tfunction getSettingsBind(param)\n\t{\n\t\treturn Utils.path(settingsPrefix, \"/\", packageIndex, \"/\", param)\n\t}\n\tfunction getServiceBind(param)\n\t{\n\t\treturn Utils.path(servicePrefix, \"/Package/\", packageIndex, \"/\", param)\n\t}\n\n    MbRowSmall\n    {\n        description: \"\"\n\n        anchors.verticalCenter: parent.verticalCenter\n\t\tColumn\n\t\t{\n\t\t\twidth: root.width - gitHubVersion.width - packageVersion.width - installedVersion.width - 20\n\t\t\tText // puts a bit of space above package name\n\t\t\t{\n\t\t\t\ttext: \" \"\n                font.pixelSize: 6\n\t\t\t}\n\t\t\tText\n\t\t\t{\n\t\t\t\ttext:packageName.valid ? packageName.value : \"\"\n\t\t\t\tcolor: isCurrentItem ? root.style.textColorSelected : root.style.textColor\n\t\t\t\tfont.pixelSize: 14\n\t\t\t\thorizontalAlignment: Text.AlignLeft\n\t\t\t}\n\t\t\tText\n\t\t\t{\n\t\t\t\ttext: statusText ()\n\t\t\t\tcolor: isCurrentItem ? root.style.textColorSelected : root.style.textColor\n\t\t\t\tfont.pixelSize: 10\n\t\t\t\thorizontalAlignment: Text.AlignLeft\n\t\t\t}\n\t\t}\n\t\tColumn\n\t\t{\n\t\t\tText // puts a bit of space above version boxes\n\t\t\t{\n\t\t\t\ttext: \" \"\n                font.pixelSize: 3\n\t\t\t}\n\t\t\tText\n\t\t\t{\n\t\t\t\ttext: \"GitHub\"\n\t\t\t\tcolor: isCurrentItem ? root.style.textColorSelected : root.style.textColor\n                font.pixelSize: 10\n\t\t\t}\n\t\t\tMbTextBlock\n\t\t\t{\n\t\t\t\tid: gitHubVersion\n\t\t\t\titem { bind: getServiceBind(\"GitHubVersion\") }\n\t\t\t\theight: 20; width: 99\n\t\t\t}\n\t\t\tText // puts a bit of space below version boxes - only needed in one column\n\t\t\t{\n\t\t\t\ttext: \" \"\n                font.pixelSize: 6\n\t\t\t}\n        }\n\t\tColumn\n\t\t{\n\t\t\tText // puts a bit of space above version boxes\n\t\t\t{\n\t\t\t\ttext: \" \"\n                font.pixelSize: 3\n\t\t\t}\n\t\t\tText\n\t\t\t{\n\t\t\t\ttext: qsTr (\"Stored\")\n\t\t\t\tcolor: isCurrentItem ? root.style.textColorSelected : root.style.textColor\n                font.pixelSize: 10\n\t\t\t}\n\t\t\tMbTextBlock\n\t\t\t{\n\t\t\t\tid: packageVersion\n\t\t\t\titem { bind: getServiceBind(\"PackageVersion\") }\n\t\t\t\theight: 20; width: 99\n\t\t\t}\n\t\t}\n\t\tColumn\n\t\t{\n\t\t\tText // puts a bit of space above version boxes\n\t\t\t{\n\t\t\t\ttext: \" \"\n                font.pixelSize: 3\n\t\t\t}\n\t\t\tText\n\t\t\t{\n\t\t\t\ttext: qsTr (\"Installed\")\n\t\t\t\tcolor: isCurrentItem ? root.style.textColorSelected : root.style.textColor\n                font.pixelSize: 10\n\t\t\t}\n\t\t\tMbTextBlock\n\t\t\t{\n\t\t\t\tid: installedVersion\n\t\t\t\titem { bind: getServiceBind(\"InstalledVersion\") }\n\t\t\t\theight: 20; width: 99\n\t\t\t}\n\t\t}\n    }\n}\n"
  },
  {
    "path": "FileSets/VersionIndependent/PageSettingsAddPackageList.qml",
    "content": "/////// new menu for package version display\n\nimport QtQuick 1.1\nimport \"utils.js\" as Utils\nimport com.victron.velib 1.0\n\nMbPage {\n\tid: root\n\ttitle: defaultCount.valid ? qsTr(\"Inactive packages (tap to activate)         \") : qsTr (\"Package manager not running\")\n\n    property string servicePrefix: \"com.victronenergy.packageManager\"\n\t// use DefaultCount as an indication that PackageManager is running\n    property VBusItem defaultCount: VBusItem { bind: Utils.path(servicePrefix, \"/DefaultCount\") }\n\n    model: defaultCount.valid ? defaultCount.value : 0\n    delegate: Component\n    {\n        MbDisplayDefaultPackage\n        {\n            servicePrefix: root.servicePrefix\n            defaultIndex: index\n        }\n    }\n}\n"
  },
  {
    "path": "FileSets/VersionIndependent/PageSettingsPackageAdd.qml",
    "content": "/////// new menu for package add edit\n\nimport QtQuick 1.1\nimport \"utils.js\" as Utils\nimport com.victron.velib 1.0\n\nMbPage {\n\tid: root\n\ttitle: editActionItem.valid ? qsTr(\"Add package\") : qsTr (\"Package manager not running\")\n\n    property bool isCurrentItem: root.ListView.isCurrentItem\n\tproperty MbStyle style: MbStyle { isCurrentItem: root.ListView.isCurrentItem }\n\n    property string settingsPrefix: \"com.victronenergy.settings/Settings/PackageManager\"\n    property string servicePrefix: \"com.victronenergy.packageManager\"\n    property int defaultIndex:0\n    property VBusItem defaultCount: VBusItem { bind: Utils.path(servicePrefix, \"/DefaultCount\") }\n    property VBusItem editActionItem: VBusItem { bind: Utils.path(servicePrefix, \"/GuiEditAction\") }\n    property VBusItem editStatus: VBusItem { bind: Utils.path(servicePrefix, \"/GuiEditStatus\") }\n    property string packageName: packageNameBox.item.valid ? packageNameBox.item.value : \"\"\n    property string editAction: editActionItem.valid ? editActionItem.value : ''\n\n    property VBusItem defaultPackageName: VBusItem { bind: Utils.path ( servicePrefix, \"/Default/\", defaultIndex, \"/\", \"PackageName\" ) }\n    property VBusItem defaultGitHubUser: VBusItem { bind: Utils.path ( servicePrefix, \"/Default/\", defaultIndex, \"/\", \"GitHubUser\" ) }\n    property VBusItem defaultGitHubBranch: VBusItem { bind: Utils.path ( servicePrefix, \"/Default/\", defaultIndex, \"/\", \"GitHubBranch\" ) }\n    property VBusItem editPackageName: VBusItem { bind: Utils.path ( settingsPrefix, \"/Edit/\", \"PackageName\" ) }\n    property VBusItem editGitHubUser: VBusItem { bind: Utils.path ( settingsPrefix, \"/Edit/\", \"GitHubUser\" ) }\n    property VBusItem editGitHubBranch: VBusItem { bind: Utils.path ( settingsPrefix, \"/Edit/\", \"GitHubBranch\" ) }\n\tproperty bool addPending: false\n\tproperty bool entryValid: editPackageName.value != \"\" && editGitHubUser.value != \"\" && editGitHubBranch.value != \"\"\n\n\tComponent.onCompleted:\n\t{\n\t\tupdateEdit ()\n\t}\n\n\tonEditActionChanged:\n\t{\n\t\tif (addPending && editAction == '')\n\t\t{\n\t\t\taddPending = false\n\t\t\tpageStack.pop()\n\t\t}\n\t}\n\n\tfunction getSettingsBind(param)\n\t{\n\t\treturn Utils.path(settingsPrefix, \"/Edit/\", param)\n\t}\n\tfunction getServiceBind(param)\n\t{\n\t\treturn Utils.path(servicePrefix, \"/Default/\", defaultIndex, \"/\", param)\n\t}\n\n\t// copy a set of default package values to Edit area when changing indexes\n\tfunction updateEdit ()\n\t{\n\t\tbindPrefix = Utils.path(servicePrefix, \"/Default/\", defaultIndex )\n\t\tvar defaultName = defaultPackageName.valid ? defaultPackageName.value : \"??\"\n\t\tif (defaultName == \"new\")\n\t\t\tdefaultName = \"\"\n\t\teditPackageName.setValue ( defaultName )\n\t\teditGitHubUser.setValue ( defaultGitHubUser.valid ? defaultGitHubUser.value : \"??\" )\n\t\teditGitHubBranch.setValue ( defaultGitHubBranch.valid ? defaultGitHubBranch.value : \"??\" )\n\t\teditStatus.setValue (\"\")\n\t\teditActionItem.setValue (\"\")\n\t\taddPending = false\n\t}\n\n    function cancelEdit ()\n    {\n\t\taddPending = false\n\t\tif (editAction == '')\n\t\t\tpageStack.pop()\n\t\telse\n\t\t{\n\t\t\teditStatus.setValue (\"\")\n\t\t\teditActionItem.setValue (\"\")\n\t\t}\n\t}\n    function confirm ()\n    {\n\t\tif (entryValid)\n\t\t{\n\t\t\taddPending = true\n\t\t\t// provide local confirmation of action - takes PackageManager too long\n\t\t\teditStatus.setValue ( \"adding \" + packageName)\n\t\t\teditActionItem.setValue ('add:' + packageName)\n\t\t}\n    }\n\tmodel: VisibleItemModel\n    {\n        MbEditBox\n        {\n            id: packageNameBox\n            description: qsTr (\"Package name\")\n            maximumLength: 30\n            item.bind: getSettingsBind (\"PackageName\")\n            overwriteMode: false\n            writeAccessLevel: User.AccessInstaller\n        }\n        MbEditBox\n        {\n            id: gitHubUserBox\n            description: qsTr (\"GitHub user\")\n            maximumLength: 20\n            item.bind: getSettingsBind (\"GitHubUser\")\n            overwriteMode: false\n            writeAccessLevel: User.AccessInstaller\n        }\n        MbEditBox\n        {\n            id: gitHubBranchBox\n            description: qsTr (\"GitHub branch or tag\")\n            maximumLength: 20\n            item.bind: getSettingsBind (\"GitHubBranch\")\n            overwriteMode: false\n            writeAccessLevel: User.AccessInstaller\n        }\n        MbOK\n        {\n            id: cancelButton\n            width: 90\n            anchors { right: parent.right  }\n            description: \"\"\n            value: editAction == '' ? qsTr(\"Cancel\") : qsTr(\"OK\")\n            onClicked: cancelEdit ()\n        }\n        MbOK\n        {\n            id: proceedButton\n            width: 100\n            anchors { right: cancelButton.left; bottom: cancelButton.bottom }\n            description: \"\"\n            value: qsTr (\"Proceed\")\n            onClicked: confirm ()\n            show: editAction == '' && entryValid\n            writeAccessLevel: User.AccessInstaller\n        }\n        Text\n        {\n            id: statusMessage\n            width: 250\n            wrapMode: Text.WordWrap\n            anchors { left: parent.left; leftMargin: 10; bottom: cancelButton.bottom; bottomMargin: 5 }\n            font.pixelSize: 12\n            text:\n            {\n\t\t\t\tif (editStatus.valid && editStatus.value != \"\")\n\t\t\t\t\treturn editStatus.value\n\t\t\t\telse if (entryValid)\n\t\t\t\t\treturn (\"add \" + packageName + \" ?\")\n\t\t\t\telse if (editPackageName.value == \"\")\n\t\t\t\t\treturn (\"enter a unique package name\")\n\t\t\t\telse if (editGitHubUser.value == \"\")\n\t\t\t\t\treturn (\"enter GitHub user\")\n\t\t\t\telse if (editGitHubBranch.value == \"\")\n\t\t\t\t\treturn (\"enter GitHub branch\")\n\t\t\t\telse\n\t\t\t\t\treturn (\"\")\n\t\t\t}\n            color: isCurrentItem ? root.style.textColorSelected : root.style.textColor\n        }\n    }\n}\n"
  },
  {
    "path": "FileSets/VersionIndependent/PageSettingsPackageEdit.qml",
    "content": "/////// new menu for package version edit\n\nimport QtQuick 1.1\nimport \"utils.js\" as Utils\nimport com.victron.velib 1.0\n\nMbPage {\n\tid: root\n\ttitle: platform.valid ? qsTr(\"Package editor\") : qsTr (\"Package manager not running\")\n\n\tproperty bool isCurrentItem: root.ListView.isCurrentItem\n\tproperty MbStyle style: MbStyle { isCurrentItem: root.ListView.isCurrentItem }\n\n\tproperty string settingsPrefix: \"com.victronenergy.settings/Settings/PackageManager\"\n\tproperty string servicePrefix: \"com.victronenergy.packageManager\"\n\tproperty int packageIndex: 0\n\tproperty int newPackageIndex:0\n\tproperty VBusItem packageCount: VBusItem { bind: Utils.path(settingsPrefix, \"/Count\") }\n\tproperty VBusItem editAction: VBusItem { bind: Utils.path(servicePrefix, \"/GuiEditAction\") }\n\tproperty VBusItem editStatus: VBusItem { bind: Utils.path(servicePrefix, \"/GuiEditStatus\") }\n\tproperty VBusItem packageNameItem: VBusItem { bind: getSettingsBind (\"PackageName\") }\n\tproperty string packageName: packageNameItem.valid ? packageNameItem.value : \"\"\n\tproperty bool isSetupHelper: packageName == \"SetupHelper\"\n\n\tproperty VBusItem incompatibleReasonItem: VBusItem { bind: getServiceBind ( \"Incompatible\" ) }\n\tproperty string incompatibleReason: incompatibleReasonItem.valid ? incompatibleReasonItem.value : \"\"\n\tproperty VBusItem incompatibleDetailsItem: VBusItem { bind: getServiceBind ( \"IncompatibleDetails\") }\n\tproperty string incompatibleDetails: incompatibleDetailsItem.valid ? incompatibleDetailsItem.value : \"\"\n\tproperty bool incompatible: incompatibleReason != \"\"\n\tproperty VBusItem platform: VBusItem { bind: Utils.path(servicePrefix, \"/Platform\") }\n\tproperty VBusItem incompatibleResolvableItem: VBusItem { bind: getServiceBind ( \"IncompatibleResolvable\") }\n\t\n\tproperty bool gitHubValid: gitHubVersion.item.valid && gitHubVersion.item.value.substring (0,1) === \"v\"\n\tproperty bool packageValid: packageVersion.item.valid && packageVersion.item.value.substring (0,1) === \"v\"\n\tproperty bool installedValid: installedVersion.item.valid && installedVersion.item.value.substring (0,1) === \"v\"\n\tproperty bool downloadOk: gitHubValid && gitHubVersion.item.value != \"\"\n\tproperty bool installOk: ! incompatible\n\tproperty string requestedAction: ''\n\tproperty bool actionPending: requestedAction != ''\n\tproperty bool waitForAction: editAction.value != '' && ! editError\n\tproperty bool editError: editAction.value == 'ERROR'\n\tproperty bool navigate: ! actionPending && ! waitForAction\n\tproperty bool detailsExist: incompatibleDetails != \"\"\n\tproperty bool detailsResolvable: incompatibleResolvableItem.valid ? incompatibleResolvableItem.value : \"\"\n\n\tproperty bool showDetails: false\n\tproperty string localError: \"\"\n\n\t// version info may be in platform service or in vePlatform.version\n\tVBusItem { id: osVersionItem; bind: Utils.path(\"com.victronenergy.platform\", \"/Firmware/Installed/Version\" ) }\n\tproperty string osVersion: osVersionItem.valid ? osVersionItem.value : vePlatform.version\n\n\t// ActionNeeded is a global parameter provided inform the GUI that a GUI restart or system reboot is needed\n\t// when dismissed, a timer started which hides the information\n\t// when the timer expires, the information is shown again\n\t// changes to ActionNeeded will stop the timer so the new value will be shown immediately\n\tproperty VBusItem actionNeededItem: VBusItem { bind: Utils.path(servicePrefix, \"/ActionNeeded\") }\n\tproperty string actionNeeded: actionNeededItem.valid ? actionNeededItem.value : \"\"\n\tproperty bool showActionNeeded: ! hideActionNeededTimer.running && actionNeeded != ''\n\n\t\n\tonActionNeededChanged:\n\t{\n\t\thideActionNeededTimer.stop ()\n\t}\n\t\n\tonWaitForActionChanged:\n\t{\n\t\tif ( ! waitForAction )\n\t\t{\n\t\t\thideActionNeededTimer.stop ()\n\t\t\trequestedAction = ''\n\t\t}\n\t}\n\n\tonIncompatibleChanged:\n\t{\n\t\tif (! incompatible )\n\t\t\tshowDetails = false\n\t}\n\n\tonActiveChanged:\n\t{\n\t\tif (active)\n\t\t{\n\t\t\thideActionNeededTimer.stop ()\n\t\t\tresetPackageIndex ()\n\t\t\trefreshGitHubVersions ()\n\t\t\tacknowledgeError ()\n\t\t\trequestedAction = ''\n\t\t}\n\t}\n\n\tonNavigateChanged: resetPackageIndex ()\n\t\n\t// hide action for 10 minutes\n\tTimer\n\t{\n\t\tid: hideActionNeededTimer\n\t\trunning: false\n\t\trepeat: false\n\t\tinterval: 1000 * 60 * 10\n\t}\n\n\t// refresh the GitHub version GitHub version age is greater than 30 seconds\n\tproperty bool waitForIndexChange: false\n\tproperty bool waitForNameChange: false\n\n\tonPackageIndexChanged:\n\t{\n\t\twaitForIndexChange = false\n\t}\n\tonPackageNameChanged:\n\t{\n\t\twaitForNameChange = false\n\t\trefreshGitHubVersions ()\n\t}\n\n\tfunction refreshGitHubVersions ()\n\t{\n\t\tif ( waitForIndexChange || waitForNameChange )\n\t\t\treturn\n\t\telse if (! active || editAction.value != \"\" || actionPending)\n\t\t\treturn\n\t\tsendCommand ( 'gitHubScan' + ':' + packageName, false )\n\t}\n\n\t// acknowledge error reported from PackageManager\n\t//\tand erase status message\n\tfunction acknowledgeError ()\n\t{\n\t\tif (editError)\n\t\t{\n\t\t\teditAction.setValue (\"\")\n\t\t\teditStatus.setValue (\"\")\n\t\t}\n\t}\n\n\tfunction resetPackageIndex ()\n\t{\n\t\tif (waitForAction)\n\t\t\treturn\n\n\t\tif (newPackageIndex < 0)\n\t\t\tnewPackageIndex = 0\n\t\telse if (newPackageIndex >= packageCount.value)\n\t\t\tnewPackageIndex = packageCount.value - 1\n\n\t\tif (newPackageIndex != packageIndex)\n\t\t{\n\t\t\twaitForIndexChange = true\n\t\t\twaitForNameChange = true\n\t\t\tpackageIndex = newPackageIndex\n\t\t\trequestedAction = ''\n\t\t\tshowDetails = false\n\t\t}\n\t}\n\n\tfunction getSettingsBind(param)\n\t{\n\t\treturn Utils.path(settingsPrefix, \"/\", packageIndex, \"/\", param)\n\t}\n\tfunction getServiceBind(param)\n\t{\n\t\treturn Utils.path(servicePrefix, \"/Package/\", packageIndex, \"/\", param)\n\t}\n\n\tfunction sendCommand (command, updateEditStatus )\n\t{\n\t\tif (editAction.value != \"\")\n\t\t\tlocalError = \"command could not be sent (\"  + command + \")\"\n\t\telse\n\t\t{\n\t\t\tif (updateEditStatus)\n\t\t\t\teditStatus.setValue (\"sending \" + command)\n\t\t\teditAction.setValue (command)\n\t\t}\n\t}\n\n\t// don't change packages if pending operation or waiting for completion\n\tfunction nextIndex ()\n\t{\n\t\tif (editError)\n\t\t\treturn\n\t\tnewPackageIndex += 1\n\t\tresetPackageIndex ()\n\t}\n\tfunction previousIndex ()\n\t{\n\t\tif (editError)\n\t\t\treturn\n\t\tnewPackageIndex -= 1\n\t\tresetPackageIndex ()\n\t}\n\n\tfunction cancelEdit ()\n\t{\n\t\t// cancel any pending operation\n\t\trequestedAction = ''\n\t\tshowDetails = false\n\n\t\tacknowledgeError ()\n\n\t\t// if was showing action needed, hide that messge for now\n\t\tif (showActionNeeded)\n\t\t\thideActionNeededTimer.start ()\n\t}\n\tfunction confirm ()\n\t{\n\t\tif (showDetails)\n\t\t{\n\t\t\tif (detailsResolvable)\n\t\t\t{\n\t\t\t\tsendCommand ( 'resolveConflicts:' + packageName, true )\n\t\t\t\tshowDetails = false\n\t\t\t}\n\t\t\t// trigger setup script prechecks\n\t\t\telse\n\t\t\t{\n\t\t\t\tsendCommand ( 'check:' + packageName, true )\n\t\t\t\tshowDetails = false\n\t\t\t}\n\t\t}\n\t\telse if (actionPending)\n\t\t\tsendCommand ( requestedAction + ':' + packageName, true )\n\t\telse if (showActionNeeded)\n\t\t{\n\t\t\tif (actionNeeded.indexOf ( \"REBOOT\" ) != -1 )\n\t\t\t\tsendCommand ( 'reboot', true )\n\t\t\telse if (actionNeeded.indexOf ( \"restart\" ) != -1 )\n\t\t\t\tsendCommand ( 'restartGui', true )\n\t\t\t\thideActionNeededTimer.start ()\n\t\t}\n\t\trequestedAction = ''\n\t}\n\tfunction install ()\n\t{\n\t\tif (navigate && installOk && ! editError)\n\t\t{\n\t\t\trequestedAction = 'install'\n\t\t\tshowDetails = false\n\t\t}\n\t}\n\tfunction uninstall ()\n\t{\n\t\tif (navigate && installedValid && ! editError)\n\t\t{\n\t\t\trequestedAction = 'uninstall'\n\t\t\tshowDetails = false\n\t\t}\n\t}\n\tfunction gitHubDownload ()\n\t{\n\t\tif (navigate && downloadOk && ! editError)\n\t\t{\n\t\t\trequestedAction = 'download'\n\t\t\tshowDetails = false\n\t\t}\n\t}\n\tfunction remove ()\n\t{\n\t\tif ( ! editError)\n\t\t{\n\t\t\trequestedAction = 'remove'\n\t\t\tshowDetails = false\n\t\t}\n\t}\n\n\tmodel: VisibleItemModel\n\t{\n\t\tMbItemText\n\t\t{\n\t\t\tid: packageNameBox\n\t\t\ttext: packageName + \" versions\"\n\t\t}\n\t\tRow\n\t\t{\n\t\t\theight: 25\n\t\t\tspacing: 1\n\t\t\t// leftPadding: 7; rightPadding: 5; can't use in earlier Qt versions\n\t\t\t// use dummy text field for left spacing instead\n\t\t\tText\n\t\t\t{\n\t\t\t\ttext: \"  \"\n\t\t\t\tfont.pixelSize: 10\n\t\t\t}\n\t\t\tText\n\t\t\t{\n\t\t\t\ttext: \"GitHub:\"\n\t\t\t\tcolor: isCurrentItem ? root.style.textColorSelected : root.style.textColor\n\t\t\t\tfont.pixelSize: 10\n\t\t\t}\n\t\t\tMbTextBlock\n\t\t\t{\n\t\t\t\tid: gitHubVersion\n\t\t\t\titem { bind: getServiceBind(\"GitHubVersion\") }\n\t\t\t\theight: 25; width: 112\n\t\t\t}\n\t\t\tText\n\t\t\t{\n\t\t\t\ttext: qsTr (\" stored:\")\n\t\t\t\tcolor: isCurrentItem ? root.style.textColorSelected : root.style.textColor\n\t\t\t\tfont.pixelSize: 10\n\t\t\t}\n\t\t\tMbTextBlock\n\t\t\t{\n\t\t\t\tid: packageVersion\n\t\t\t\titem { bind: getServiceBind(\"PackageVersion\") }\n\t\t\t\theight: 25; width: 112\n\t\t\t}\n\t\t\tText\n\t\t\t{\n\t\t\t\ttext: qsTr (\" installed:\")\n\t\t\t\tcolor: isCurrentItem ? root.style.textColorSelected : root.style.textColor\n\t\t\t\thorizontalAlignment: Text.AlignRight\n\t\t\t\tfont.pixelSize: 10\n\t\t\t}\n\t\t\tMbTextBlock\n\t\t\t{\n\t\t\t\tid: installedVersion\n\t\t\t\titem { bind: getServiceBind(\"InstalledVersion\") }\n\t\t\t\theight: 25\n\t\t\t\twidth: 112\n\t\t\t}\n\t\t}\n\t\tMbEditBox\n\t\t{\n\t\t\tid: gitHubUser\n\t\t\tdescription: qsTr (\"GitHub user\")\n\t\t\tmaximumLength: 20\n\t\t\titem.bind: getSettingsBind (\"GitHubUser\")\n\t\t\toverwriteMode: false\n\t\t\twriteAccessLevel: User.AccessInstaller\n\t\t}\n\t\tMbEditBox\n\t\t{\n\t\t\tid: gitHubBranch\n\t\t\tdescription: qsTr (\"GitHub branch or tag\")\n\t\t\tmaximumLength: 20\n\t\t\titem.bind: getSettingsBind (\"GitHubBranch\")\n\t\t\toverwriteMode: false\n\t\t\twriteAccessLevel: User.AccessInstaller\n\t\t}\n\n\t\tMbOK\n\t\t{\n\t\t\tid: cancelButton\n\t\t\twidth: 85\n\t\t\tanchors { right: gitHubBranch.right; bottom: statusMessage.bottom }\n\t\t\tdescription: \"\"\n\t\t\tvalue: ( actionPending || showDetails ) ? qsTr(\"Cancel\") : (editError ? qsTr(\"OK\") : qsTr(\"Later\"))\n\t\t\tonClicked: cancelEdit ()\n\t\t\tshow: ( actionPending || showDetails || editError || showActionNeeded ) && ! waitForAction\n\t\t}\n\t\tMbOK\n\t\t{\n\t\t\tid: confirmButton\n\t\t\twidth: 92\n\t\t\tanchors { right: cancelButton.left; bottom: statusMessage.bottom }\n\t\t\tdescription: \"\"\n\t\t\tvalue: ( actionPending || detailsResolvable ) ? qsTr(\"Proceed\") : showDetails ? qsTr (\"Recheck\") : qsTr (\"Now\")\n\t\t\tonClicked: confirm ()\n\t\t\tshow: ( actionPending || showDetails || showActionNeeded ) && ! waitForAction\n\t\t\twriteAccessLevel: User.AccessInstaller\n\t\t}\n\t\tMbOK\n\t\t{\n\t\t\tid: showDetailsButton\n\t\t\twidth: 150\n\t\t\tanchors { right: gitHubBranch.right; bottom: statusMessage.bottom}\n\t\t\tdescription: \"\"\n\t\t\tvalue: qsTr(\"Show Details\")\n\t\t\tonClicked: showDetails = true\n\t\t\twriteAccessLevel: User.AccessInstaller\n\t\t\tshow: navigate && detailsExist && ! ( editError || actionPending || waitForAction || showActionNeeded || showDetails)\n\t\t}\n\t\tText\n\t\t{\n\t\t\tid: statusMessage\n\t\t\twidth:\n\t\t\t{\n\t\t\t\tvar smWidth = root.width\n\t\t\t\tif (cancelButton.show)\n\t\t\t\t\tsmWidth -= cancelButton.width\n\t\t\t\tif (confirmButton.show)\n\t\t\t\t\tsmWidth -= confirmButton.width\n\t\t\t\tif (showDetailsButton.show)\n\t\t\t\t\tsmWidth -= showDetailsButton.width\n\t\t\t\treturn smWidth\n\t\t\t}\n\t\t\theight: Math.max (paintedHeight, 35)\n\t\t\twrapMode: Text.WordWrap\n\t\t\thorizontalAlignment: Text.AlignLeft\n\t\t\tanchors { left: gitHubBranch.left; leftMargin: 5; top: gitHubBranch.bottom }\n\t\t\tfont.pixelSize: 12\n\t\t\tcolor: isSetupHelper && requestedAction == 'uninstall' ? \"red\" : root.style.textColor\n\t\t\ttext:\n\t\t\t{\n\t\t\t\tif (showDetails)\n\t\t\t\t{\n\t\t\t\t\tif (detailsResolvable)\n\t\t\t\t\t\treturn ( incompatibleDetails + qsTr (\"\\nResolve conflicts?\") )\n\t\t\t\t\telse\n\t\t\t\t\t\treturn ( incompatibleDetails )\n\t\t\t\t}\n\t\t\t\telse if (actionPending)\n\t\t\t\t{\n\t\t\t\t\tif (isSetupHelper && requestedAction == 'uninstall')\n\t\t\t\t\t\treturn qsTr (\"WARNING: SetupHelper is required for these menus - uninstall anyway ?\")\n\t\t\t\t\telse\n\t\t\t\t\t\treturn (requestedAction + \" \" + packageName + \" ?\")\n\t\t\t\t}\n\t\t\t\telse if (editStatus.valid && editStatus.value != \"\")\n\t\t\t\t\treturn ( editStatus.value )\n\t\t\t\telse if (showActionNeeded)\n\t\t\t\t\treturn ( actionNeeded ) \n\t\t\t\telse if (incompatible)\n\t\t\t\t\treturn ( incompatibleReason )\n\t\t\t\telse\n\t\t\t\t\treturn localError\n\t\t\t}\n\t\t}\n\t\t// bottom row of buttons\n\t\tMbOK\n\t\t{\n\t\t\tid: previousButton\n\t\t\twidth: 100\n\t\t\tanchors { left: gitHubBranch.left; top: statusMessage.bottom; topMargin: 5  }\n\t\t\tdescription: \"\"\n\t\t\tvalue: qsTr(\"Previous\")\n\t\t\tonClicked: previousIndex ()\n\t\t\topacity: ! editError && newPackageIndex > 0 ? 1.0 : 0.2\n\t\t}\n\t\tMbOK\n\t\t{\n\t\t\tid: nextButton\n\t\t\twidth: 70\n\t\t\tanchors { left: previousButton.right; top: statusMessage.bottom; topMargin: 5 }\n\t\t\tdescription: \"\"\n\t\t\tvalue: qsTr(\"Next\")\n\t\t\tonClicked: nextIndex ()\n\t\t\topacity: ! editError && (newPackageIndex < packageCount.value - 1) ? 1.0 : 0.2\n\t\t}\n\t\tMbOK\n\t\t{\n\t\t\tid: downloadButton\n\t\t\twidth: 110\n\t\t\tanchors { right: installButton.left; top: statusMessage.bottom; topMargin: 5 }\n\t\t\tdescription: \"\"\n\t\t\tvalue: qsTr (\"Download\")\n\t\t\tonClicked: gitHubDownload ()\n\t\t\topacity: ! editError && navigate && downloadOk > 0 ? 1.0 : 0.2\n\t\t\twriteAccessLevel: User.AccessInstaller\n\t\t}\n\t\tMbOK\n\t\t{\n\t\t\tid: installButton\n\t\t\twidth: 80\n\t\t\tanchors { right: uninstallButton.left; top: statusMessage.bottom; topMargin: 5 }\n\t\t\tdescription: \"\"\n\t\t\tvalue: qsTr (\"Install\")\n\t\t\tonClicked: install ()\n\t\t\topacity: ! editError && navigate && installOk > 0 ? 1.0 : 0.2\n\t\t\twriteAccessLevel: User.AccessInstaller\n\t\t}\n\t\tMbOK\n\t\t{\n\t\t\tid: uninstallButton\n\t\t\twidth: 105\n\t\t\tanchors { right: gitHubBranch.right; top: statusMessage.bottom; topMargin: 5 }\n\t\t\tdescription: \"\"\n\t\t\tvalue: installedValid ? qsTr(\"Uninstall\") : qsTr(\"Remove\")\n\t\t\tonClicked: installedValid ? uninstall () : remove ()\n\t\t\topacity: ! editError && navigate ? 1.0 : 0.2\n\t\t\twriteAccessLevel: User.AccessInstaller\n\t\t}\n\t\t// dummy item to allow scrolling to show last button line when status message has many lines\n\t\tMbItemText\n\t\t{\n\t\t\ttext: \"\"\n\t\t\topacity: 0\n\t\t\tshow: statusMessage.height > 35\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "FileSets/VersionIndependent/PageSettingsPackageManager.qml",
    "content": "/////// new menu for package version display\n\nimport QtQuick 1.1\nimport \"utils.js\" as Utils\nimport com.victron.velib 1.0\n\nMbPage {\n\tid: root\n\ttitle: showControls ? qsTr(\"Package manager\") : qsTr(\"Package manager not running\")\n    property string settingsPrefix: \"com.victronenergy.settings/Settings/PackageManager\"\n    property string servicePrefix: \"com.victronenergy.packageManager\"\n\tproperty string bindVrmloggerPrefix: \"com.victronenergy.logger\"\n    VBusItem { id: pmStatusItem; bind: Utils.path(servicePrefix, \"/PmStatus\") }\n\tproperty string pmStatus: pmStatusItem.valid ? pmStatusItem.value : \"\"\n    VBusItem { id: mediaStatus; bind: Utils.path(servicePrefix, \"/MediaUpdateStatus\") }\n    VBusItem { id: actionNeeded; bind: Utils.path(servicePrefix, \"/ActionNeeded\") }\n    VBusItem { id: editAction; bind: Utils.path(servicePrefix, \"/GuiEditAction\") }\n    property bool showMediaStatus: mediaStatus.valid && mediaStatus.value != \"\"\n    property bool showControls: pmStatusItem.valid\n\n\t// the last status message received from PackageManager is saved in lastStatus\n\t//\tso there is some status to display when PackageManager quits\n\tproperty string lastStatus: \"\"\n\n\tonPmStatusChanged:\n\t{\n\t\tif (pmStatusItem.valid)\n\t\t\tlastStatus = pmStatus\n\t}\n\n\tmodel: VisibleItemModel\n    {\n        MbItemText\n        {\n\t\t\tid: status\n            text:\n            {\n\t\t\t\tif (mediaStatus.valid && mediaStatus.value != \"\")\n\t\t\t\t\treturn mediaStatus.value\n\t\t\t\telse if (showControls)\n\t\t\t\t\treturn pmStatusItem.value\n\t\t\t\telse\n\t\t\t\t\treturn lastStatus\n\t\t\t}\n            wrapMode: Text.WordWrap\n            horizontalAlignment: Text.AlignHCenter\n        }\n        MbItemOptions\n        {\n            id: autoDownload\n            description: qsTr (\"GitHub check frequency\")\n            bind: Utils.path (settingsPrefix, \"/GitHubAutoDownload\")\n            possibleValues:\n            [\n\t\t\t\tMbOption { description: \"Once\"; value: 99 },\n\t\t\t\tMbOption { description: \"Every 10 minutes\"; value: 1 },\n\t\t\t\tMbOption { description: \"Hourly\"; value: 2 },\n\t\t\t\tMbOption { description: \"Daily\"; value: 3 },\n                MbOption { description: \"Never\"; value: 0 }\n            ]\n            writeAccessLevel: User.AccessInstaller\n        }\n        MbSwitch\n        {\n            id: autoInstall\n            bind: Utils.path (settingsPrefix, \"/AutoInstall\")\n            name: qsTr (\"Auto install packages\")\n            writeAccessLevel: User.AccessInstaller\n        }\n        MbSubMenu\n        {\n            description: qsTr(\"Active packages\")\n            subpage: Component { PageSettingsPackageVersions {} }\n            show: showControls\n        }\n\t\tMbSubMenu\n        {\n            description: qsTr(\"Inactive packages\")\n            subpage: Component { PageSettingsAddPackageList {} }\n            show: showControls\n        }\n        MbOK\n        {\n            id: finishButton\n            description:\n            {\n\t\t\t\tif (editAction.value == 'reboot')\n\t\t\t\t\treturn qsTr (\"REBOOTING ...\")\n\t\t\t\telse if (editAction.value == 'guiRestart')\n\t\t\t\t\treturn qsTr (\"restarting GUI ...\")\n\t\t\t\telse\n\t\t\t\t\treturn qsTr (\"action to finish install/uninstall\")\n\t\t\t}\n            value:\n             {\n\t\t\t\tif (! actionNeeded.valid)\n\t\t\t\t\treturn \"\"\n\t\t\t\telse if (actionNeeded.value.indexOf ( \"REBOOT\" ) != -1 )\n\t\t\t\t\treturn qsTr (\"Reboot\")\n\t\t\t\telse if (actionNeeded.value.indexOf ( \"restart\" ) != -1 )\n\t\t\t\t\treturn qsTr (\"Restart GUI\")\n\t\t\t\telse\n\t\t\t\t\treturn \"\"\n\t\t\t}\n\t\t\tonClicked:\n            {\n\t\t\t\tif (finishButton.value == 'REBOOT')\n\t\t\t\t{\n\t\t\t\t\t// needs immediate update because GUI will be going down ASAP\n\t\t\t\t\tfinishButton.description = qsTr (\"REBOOTING ...\")\n\t\t\t\t\teditAction.setValue ( 'reboot' )\n\t\t\t\t}\n\t\t\t\telse if (finishButton.value == 'guiRestart')\n\t\t\t\t{\n\t\t\t\t\t// needs immediate update because GUI will be going down ASAP\n\t\t\t\t\tfinishButton.description = qsTr (\"restarting GUI ...\")\n\t\t\t\t\teditAction.setValue ( 'restartGui' )\n\t\t\t\t}\n\t\t\t}\n            show: actionNeeded.valid && actionNeeded.value != ''\n            writeAccessLevel: User.AccessInstaller\n        }\n\t\tMbSubMenu\n        {\n            description: qsTr(\"Backup & restore settings\")\n            subpage: Component { PageSettingsPmBackup {} }\n            show: showControls\n        }\n\t\tMbOK {\n\t\t\tproperty int notMounted: 0\n\t\t\tproperty int mounted: 1\n\t\t\tproperty int unmountRequested: 2\n\t\t\tproperty int unmountBusy: 3\n\n\t\t\tfunction mountStateToText(s)\n\t\t\t{\n\t\t\t\tswitch (s) {\n\t\t\t\tcase mounted:\n\t\t\t\t\treturn qsTr(\"Press to eject\");\n\t\t\t\tcase unmountRequested:\n\t\t\t\tcase unmountBusy:\n\t\t\t\t\treturn qsTr(\"Ejecting, please wait\");\n\t\t\t\tdefault:\n\t\t\t\t\treturn qsTr(\"No storage found\");\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tVBusItem {\n\t\t\t\tid: vMountState\n\t\t\t\tbind: Utils.path(bindVrmloggerPrefix, \"/Storage/MountState\")\n\t\t\t}\n\t\t\tdescription: qsTr(\"microSD / USB\")\n\t\t\tvalue: mountStateToText(vMountState.value)\n\t\t\twriteAccessLevel: User.AccessUser\n\t\t\tonClicked: vMountState.setValue(unmountRequested);\n\t\t\teditable: vMountState.value === mounted\n\t\t\tcornerMark: false\n\t\t}\n\t\tMbSubMenu\n        {\n            description: qsTr(\"Restart or initialize ...\")\n            subpage: Component { PageSettingsPmInitialize {} }\n            show: showControls\n        }\n    }\n}\n"
  },
  {
    "path": "FileSets/VersionIndependent/PageSettingsPackageVersions.qml",
    "content": "/////// new menu for package version display\n\nimport QtQuick 1.1\nimport \"utils.js\" as Utils\nimport com.victron.velib 1.0\n\nMbPage {\n\tid: root\n\ttitle: defaultCount.valid ? qsTr(\"Active packages (tap to edit)        \") : qsTr (\"Package manager not running\")\n    property string servicePrefix: \"com.victronenergy.packageManager\"\n    property string settingsPrefix: \"com.victronenergy.settings/Settings/PackageManager\"\n    property VBusItem count: VBusItem { bind: Utils.path(settingsPrefix, \"/Count\") }\n\t// use DefaultCount as an indication that PackageManager is running\n    property VBusItem defaultCount: VBusItem { bind: Utils.path(servicePrefix, \"/DefaultCount\") }\n\tproperty VBusItem editAction: VBusItem { bind: Utils.path(servicePrefix, \"/GuiEditAction\") }\n\n\t// notify PackageManager to refresh GitHub versions for all packages\n\t// when this menu goes active (entering from parent or returning from child)\n\t// or if first package's GitHub version age is greater than 60 seconds\n\tonActiveChanged: refreshGitHubVersions ()\n\n\tfunction refreshGitHubVersions ()\n\t{\n\t\tif (! active)\n\t\t\treturn\n\t\telse if ( editAction.value != \"\" )\n\t\t\treturn\n\n\t\teditAction.setValue ('gitHubScan:ALL')\n\t}\n\n\n\tmodel: defaultCount.valid ? count.valid ? count.value : 0 : 0\n    delegate: Component\n    {\n        MbDisplayPackageVersion\n        {\n            servicePrefix: root.servicePrefix\n            settingsPrefix: root.settingsPrefix\n            packageIndex: index\n        }\n    }\n}\n"
  },
  {
    "path": "FileSets/VersionIndependent/PageSettingsPmBackup.qml",
    "content": "/////// new menu for settings backup and restore\n\nimport QtQuick 1.1\nimport \"utils.js\" as Utils\nimport com.victron.velib 1.0\n\nMbPage {\n\tid: root\n\ttitle: qsTr(\"Settings backup & restore\")\n    property string settingsPrefix: \"com.victronenergy.settings/Settings/PackageManager\"\n    property string servicePrefix: \"com.victronenergy.packageManager\"\n\tVBusItem { id: mediaAvailable; bind: Utils.path(servicePrefix, \"/BackupMediaAvailable\") }\n\tVBusItem { id: settingsFileExists; bind: Utils.path(servicePrefix, \"/BackupSettingsFileExist\") }\n\tVBusItem { id: settingsLocalFileExists; bind: Utils.path(servicePrefix, \"/BackupSettingsLocalFileExist\") }\n\tVBusItem { id: backupProgressItem; bind: Utils.path(servicePrefix, \"/BackupProgress\") }\n\tproperty int backupProgress: backupProgressItem.valid ? backupProgressItem.value : 0\n\n\tmodel: VisibleItemModel\n    {\n        MbItemText\n        {\n\t\t\tid: info\n            text: qsTr (\"Backup and restore\\nSOME system settings, logs and logos\\nthis is NOT the Victron mechanism\\ncurrently under development\")\n            wrapMode: Text.WordWrap\n            horizontalAlignment: Text.AlignHCenter\n        }\n        MbItemText\n        {\n\t\t\tid: status\n            text:\n\t\t\t{\n\t\t\t\tif (backupProgress == 21 || backupProgress == 23)\n\t\t\t\t\treturn qsTr (\"backing up settings to local storage ... (may take a while)\")\n\t\t\t\telse if (backupProgress == 22 || backupProgress == 24)\n\t\t\t\t\treturn qsTr (\"restoring settings from local storage ... (may take a while)\")\n\t\t\t\telse if (backupProgress == 1 || backupProgress == 3)\n\t\t\t\t\treturn qsTr (\"backing up settings ... (may take a while)\")\n\t\t\t\telse if (backupProgress == 2 || backupProgress == 4)\n\t\t\t\t\treturn qsTr (\"restoring settings ... (may take a while)\")\n\t\t\t\telse if ( ! mediaAvailable.valid || mediaAvailable.value == 0)\n\t\t\t\t\treturn qsTr (\"No USB or SD media found - insert one to continue\")\n\t\t\t\telse if (settingsFileExists.valid && settingsFileExists.value == 1)\n\t\t\t\t\treturn qsTr (\"Settings backup file found\")\n\t\t\t\telse\n\t\t\t\t\treturn \"\"\n\t\t\t}\n            wrapMode: Text.WordWrap\n            horizontalAlignment: Text.AlignHCenter\n        }\n\t\tMbOK\n\t\t{\n\t\t\tdescription: qsTr(\"Backup settings, logos, logs\")\n\t\t\tvalue: qsTr(\"Press to backup\")\n\t\t\tonClicked: backupProgressItem.setValue (1)\n\t\t\tshow: mediaAvailable.valid && mediaAvailable.value == 1 && backupProgressItem.value == 0\n            writeAccessLevel: User.AccessInstaller\n\t\t}\n\t\tMbOK\n\t\t{\n\t\t\tdescription: qsTr(\"Restore settings, logos\")\n\t\t\tvalue: qsTr(\"Press to restore\")\n\t\t\tonClicked: backupProgressItem.setValue (2)\n\t\t\tshow: settingsFileExists.valid && settingsFileExists.value == 1 && backupProgressItem.value == 0\n            writeAccessLevel: User.AccessInstaller\n\t\t}\n\t\tMbOK\n\t\t{\n\t\t\tdescription: qsTr(\"Backup settings to local storage\")\n\t\t\tvalue: qsTr(\"Press to backup\")\n\t\t\tonClicked: backupProgressItem.setValue (21)\n\t\t\tshow: backupProgressItem.value == 0\n            writeAccessLevel: User.AccessInstaller\n\t\t}\n\t\tMbOK\n\t\t{\n\t\t\tdescription: qsTr(\"Restore settings to from storage\")\n\t\t\tvalue: qsTr(\"Press to restore\")\n\t\t\tonClicked: backupProgressItem.setValue (22)\n\t\t\tshow: settingsLocalFileExists.valid && settingsLocalFileExists.value == 1 && backupProgressItem.value == 0\n            writeAccessLevel: User.AccessInstaller\n\t\t}\n    }\n}\n"
  },
  {
    "path": "FileSets/VersionIndependent/PageSettingsPmInitialize.qml",
    "content": "/////// new menu for PackageManager initialize\n\nimport QtQuick 1.1\nimport \"utils.js\" as Utils\nimport com.victron.velib 1.0\n\nMbPage {\n\tid: root\n\ttitle: pmRunning ? qsTr(\"PackageManager restart/initialize\") : qsTr (\"Package manager not running\")\n    property string settingsPrefix: \"com.victronenergy.settings/Settings/PackageManager\"\n\tVBusItem { id: pmStatus; bind: Utils.path(servicePrefix, \"/PmStatus\") }\n\tproperty bool pmRunning: pmStatus.valid\n\n\tproperty bool showInProgress: false\n\tproperty string initializeMessage: \"\"\n\n\tonPmRunningChanged: { showInProgress = false }\n\n    function sendCommand (command, message)\n    {\n\t\tinitializeMessage = message\n\t\tshowInProgress = true\n\t\teditAction.setValue (command)\n    }\n\n\tmodel: VisibleItemModel\n    {\n\t\tMbOK\n\t\t{\n\t\t\tdescription: qsTr(\"Restart\")\n\t\t\tvalue: qsTr(\"Press to restart Package Manager\")\n\t\t\tonClicked:sendCommand (\"RESTART_PM\", qsTr (\"restarting Package Manager ...\"))\n\t\t\twriteAccessLevel: User.AccessInstaller\n\t\t\tshow: ! showInProgress\n\t\t}\n\t\tMbOK\n\t\t{\n\t\t\tdescription: qsTr(\"Restart GUI\")\n\t\t\tvalue: qsTr(\"Press to restart GUI\")\n\t\t\tonClicked:sendCommand (\"restartGui\", qsTr (\"restarting GUI ...\"))\n\t\t\twriteAccessLevel: User.AccessInstaller\n\t\t\tshow: ! showInProgress\n\t\t}\n        MbItemText\n        {\n\t\t\tid: info\n            text: qsTr (\"Initializing PackageManager will\\nreset persistent storage to an empty state\\nGit Hub user and branch are reset to defaults\\nPackages added manually must be added again\")\n            wrapMode: Text.WordWrap\n            horizontalAlignment: Text.AlignHCenter\n\t\t\tshow: ! showInProgress\n        }\n\t\tMbOK\n\t\t{\n\t\t\tdescription: qsTr(\"Initialize\")\n\t\t\tvalue: qsTr(\"Press to INITIALIZE Package Manager\")\n\t\t\tonClicked: sendCommand (\"INITIALIZE_PM\", qsTr (\"INITIALIZING Package Manager ...\"))\n            writeAccessLevel: User.AccessInstaller\n            show: ! showInProgress\n\t\t}\n        MbItemText\n        {\n\t\t\tid: initializingMessage\n            text: initializeMessage\n            wrapMode: Text.WordWrap\n            horizontalAlignment: Text.AlignHCenter\n            show: showInProgress\n        }\n    }\n}\n"
  },
  {
    "path": "FileSets/fileListPatched",
    "content": "/opt/victronenergy/gui/qml/PageSettings.qml\n"
  },
  {
    "path": "FileSets/fileListVersionIndependent",
    "content": "/opt/victronenergy/gui/qml/PageSettingsPackageManager.qml\n/opt/victronenergy/gui/qml/PageSettingsPackageVersions.qml\n/opt/victronenergy/gui/qml/PageSettingsPackageEdit.qml\n/opt/victronenergy/gui/qml/PageSettingsAddPackageList.qml\n/opt/victronenergy/gui/qml/PageSettingsPackageAdd.qml\n/opt/victronenergy/gui/qml/PageSettingsPmBackup.qml\n/opt/victronenergy/gui/qml/PageSettingsPmInitialize.qml\n/opt/victronenergy/gui/qml/MbDisplayPackageVersion.qml\n/opt/victronenergy/gui/qml/MbDisplayDefaultPackage.qml\n"
  },
  {
    "path": "HelperResources/CommonResources",
    "content": "#!/bin/bash\n\n\n# CommonResources for SetupHelper\n# contains a functions and variables necessary for a setup script to interface with reinstallMods\n#\n# Refer to the SetupHelper ReadMe file for details on how to use these resources.\n\n# what action the script should take:\n#  NONE - do noting - signals script to prompt for user input on how to proceed\n#  INSTALL - install package components\n#  (decommissioned) PROMPT - prompt user for additional installation options\n#  UNINSTALL - remove package components\n#  EXIT - exit script without taking any action\n#  CHECK - runs file set checks only\n#\t\tthis will attempt to create a missing file set so PackageManager\n#\t\twon't report it missing\n# CommonResources may set the the action if initial checks\n#  indicate a clear direction\n# otherwise, the action will be set based on user input (in the script)\n# if failures occur during installation,\n# scriptAction should be changed to UNINSTALL so the installation can be cleaned up\n# and the setup script should test for UNINSTALL after it attempts installation\n# A file set error indicates the file set for the current verion is not usable\n#  and installation should not occur\n#  checkFileSets EXITS locally\n\nscriptAction='NONE'\n\n# flags to control setup script behavior (in endScript)\nrebootNeeded=false\nrunAgain=false\nfilesUpdated=false\nrestartGui=false\nrestartGeneratorService=false\nrestartSystemCalc=false\nrestartDigitalinputs=false\n\n# file lists are populated by getFileLists called from_chckFileSets and autoinstall\n#\tso these are global\nfileList=()\nfileListVersionIndependent=()\nfileListAll=()\n\n\n######## skip to bottom of file for remainder of code executed when script is sourced\n\n\n# cleanup on any exit\nexitCleanup ()\n{\n\t# remove temp directory\n\trm -rf \"$tempFileDir\"\n}\n\ntrap exitCleanup EXIT\n\n# checkPackageDependencies checks the packageDependencies file in the package directory\n#\n# all unmet dependencies are reported to the command line/log\n#\tand to stderr if not running from the command line\n# then the script exists\n\nfunction checkPackageDependencies ()\n{\n\tdependencyFile=\"$scriptDir/packageDependencies\"\n\n\t#no dependencies specified for this package\n\tif ! [ -f \"$dependencyFile\" ]; then return; fi\n\n\terrors=\"\"\n\twhile IFS= read -r line; do\n\t\terror=\"\"\n\t\tread package requirement <<< \"$line\"\n\t\tif [ -f \"$installedVersionPrefix\"$package ]; then\n\t\t\tpackageInstalled=true\n\t\telse\n\t\t\tpackageInstalled=false\n\t\tfi\n\t\tcase $requirement in\n\t\t\tinstalled)\n\t\t\t\tif ! $packageInstalled ; then\n\t\t\t\t\terror=\"$package must be installed\"\n\t\t\t\tfi\n\t\t\t\t;;\n\t\t\tuninstalled)\n\t\t\t\tif $packageInstalled ; then\n\t\t\t\t\terror=\"$package must be uninstalled\"\n\t\t\t\tfi\n\t\t\t\t;;\n\t\tesac\n\t\tif ! [ -z \"$error\" ]; then\n\t\t\tif ! [ -z \"$errors\" ]; then\n\t\t\t\terrors+=\", \"\n\t\t\tfi\n\t\t\terrors+=\"$error\"\n\t\tfi\n\n\tdone < \"$dependencyFile\"\n\tif ! [ -z \"$errors\" ]; then\n\t\tsetInstallFailed $EXIT_PACKAGE_CONFLICT \"$errors\"\n\tfi\n}\n\n# getFileLists reads the file list from files in the FileSets directory\n#\n#\t'fileList' file must only list version-dependent files\n#\t'fileListVersionIndependent' file must list only version-independent files\n#\t\tprior to SetupHelper v6.0, this list is ignored\n#\t'fileListPatched' lists all files that should be patched before installation\n#\n# $1 specifies where the path to the fileList files\n#\n# three composite file lists are returned in global arrays:\n#\tfileList contains only version-dependent files\n#\tfileListVersionIndependent contains only version-independent files\n#\tfileListPatched contains only files that need to be patched\n#\tfileListAll contains both versioned and version-independent files\n\nfunction getFileLists ()\n{\n\tlocal verListFile=\"$1/fileList\"\n\tlocal indListFile=\"$1/fileListVersionIndependent\"\n\tlocal patchListFile=\"$1/fileListPatched\"\n\tlocal tempListVer=()\n\tlocal tempListInd=()\n\tlocal tempListPatched=()\n\n\tif [ -f \"$verListFile\" ]; then\n\t\twhile read -r line || [[ -n \"$line\" ]]; do\n\t\t\tread -a params <<< $line\n\t\t\t# parse line into space-separted parameters then discard any that don't begin with /\n\t\t\t# this strips all comments beginning with # as well as any leading or trailing spaces\n\t\t\tfor param in ${params[@]} ; do\n\t\t\t\tcase $param in\n\t\t\t\t\t/*)\n\t\t\t\t\t\ttempListVer+=(\"$param\")\n\t\t\t\t\t\t;;\n\t\t\t\tesac\n\t\t\tdone\n\t\tdone < \"$verListFile\"\n\tfi\n\tif [ -f \"$indListFile\" ]; then\n\t\twhile read -r line || [[ -n \"$line\" ]]; do\n\t\t\tread -a params <<< $line\n\t\t\tfor param in ${params[@]} ; do\n\t\t\t\tcase $param in\n\t\t\t\t\t/*)\n\t\t\t\t\t\ttempListInd+=(\"$param\")\n\t\t\t\t\t\t;;\n\t\t\t\tesac\n\t\t\tdone\n\t\tdone < \"$indListFile\"\n\tfi\n\tif [ -f \"$patchListFile\" ]; then\n\t\twhile read -r line || [[ -n \"$line\" ]]; do\n\t\t\tread -a params <<< $line\n\t\t\tfor param in ${params[@]} ; do\n\t\t\t\tcase $param in\n\t\t\t\t\t/*)\n\t\t\t\t\t\ttempListPatched+=(\"$param\")\n\t\t\t\t\t\t;;\n\t\t\t\tesac\n\t\t\tdone\n\t\tdone < \"$patchListFile\"\n\tfi\n\n\t# remove duplicate files from each list\n\tfileList=($(printf \"%s\\n\" \"${tempListVer[@]}\" | sort -u))\n\tfileListVersionIndependent=($(printf \"%s\\n\" \"${tempListInd[@]}\" | sort -u))\n\tfileListPatched=($(printf \"%s\\n\" \"${tempListPatched[@]}\" | sort -u))\n\ttempListAll=(${fileList[@]})\n\ttempListAll+=(${fileListVersionIndependent[@]})\n\ttempListAll+=(${fileListPatched[@]})\n\tfileListAll=($(printf \"%s\\n\" \"${tempListAll[@]}\" | sort -u))\n}\n\n\n# yesNoPrompt provides user prompting requesting a yes/no response\n#\n# $1 is the prompt displayed when pausing for user input\n#\n# $yesResponse is set to true if the response was yes\n#\n# returns 0 for yes, 1 for no\n\nyesNoPrompt ()\n{\n    response=''\n    while true; do\n        /bin/echo -n \"$*\"\n        read response\n        case $response in\n            [yY]*)\n                yesResponse=true\n                return 0\n                break\n                ;;\n            [nN]*)\n                yesResponse=false\n                return 1\n                break\n                ;;\n            *)\n        esac\n    done\n}\n\n\n# standardActionPrompt provides the standard set of options for selecting script's action\n# scriptAction is set by install/uninstall\n# other actions are handled locally, including quitting from the script\n#\n# if nonstandard prompts are necessary, duplicate this code in the setup script\n# and add the additional options and do not call standardActionPrompt\n#\n# the reinstall option is permitted only if setup options were previously set\n# if the the reinstall action is choosen, the script action is set to INSTALL\n# the setup script can then test this to skip further prompts\n#\n# $1 indicates if there are additional prompts needed during installaiton\n# if this parameter is 'MORE_PROMPTS', installaiton does NOT change scriptAction\n# if this parameter does not exist, installation WILL change scriptAction to INSTALL\n# this provides backaward compatibility with scripts written prior to the reinstall logic\n#\nstandardActionPrompt ()\n{\n    if [ $# -gt 0 ] && [ $1 == 'MORE_PROMPTS' ]; then\n        updateScriptAction=false\n    else\n        updateScriptAction=true\n    fi\n\n    echo\n    echo \"Available actions:\"\n\t# don't allow install choice if incompatibilities have already been detected\n\tif ! $installFailed ; then\n\t\techo \"  Install and activate (i)\"\n\telse\n\t\techo \"  can't install - errors reported above\"\n\tfi\n    if $optionsSet ; then\n        echo \"  Reinstall (r) based on options provided at last install\"\n    fi\n    echo \"  Uninstall (u) and restores all files to stock\"\n    echo \"  Quit (q) without further action\"\n    echo \"  Display log (l) outputs the last 100 lines of the log\"\n    echo\n    response=''\n    while true; do\n        /bin/echo -n \"Choose an action from the list above: \"\n        read response\n        case $response in\n            [iI]*)\n\t\t\t\tif ! $installFailed ; then\n\t\t\t\t\tif $updateScriptAction ; then\n\t\t\t\t\t\tscriptAction='INSTALL'\n\t\t\t\t\tfi\n\t\t\t\t\tbreak\n\t\t\t\tfi\n                ;;\n            [rR]*)\n                if $optionsSet ; then\n                    scriptAction='INSTALL'\n                    break\n                fi\n                ;;\n            [uU]*)\n                scriptAction='UNINSTALL'\n                break\n                ;;\n            [qQ]*)\n                exit $EXIT_SUCCESS\n                ;;\n            [lL]*)\n\t\t\t\ttail -100 \"$logFile\" | tai64nlocal\n                ;;\n            *)\n        esac\n    done\n}\n\n\n# forcePackageUninstall insures a conflicting package is uninstalled before\n#\tthis package is installed\n# the setup script must call this script BEFORE it begins installing anything\n# $1 is the package name\n# $2 contains an optional message\n\nforcePackageUninstall ()\n{\n\tif (( $# < 1 )); then\n\t\treturn\n\tfi\n\tif [ -f \"$installedVersionPrefix\"\"$1\" ]; then\n\t\tif (( $# >= 2 )); then\n\t\t\tlogMessage \"${@:2}\"\n\t\telse\n\t\t\tlogMessage \"uninstalling $1 - it conflicts with $packageName\"\n\t\tfi\n\t\tif [ -e \"/data/$1/setup\" ]; then\n\t\t\t\"/data/$1/setup\" \"uninstall\" \"auto\" \"deferReboot\" \"deferGuiRestart\"\n\t\telse\n\t\t\tlogMessage \"WARNING can't uninstall $1 - no package directory or no setup script\"\n\t\tfi\n\t\tif [ -e \"/data/settupOptions/$1\" ]; then\n\t\t\ttouch \"/data/settupOptions/$1/DO_NOT_AUTO_INSTALL\"\n\t\tfi\n\tfi\n}\n\n\n# backupActiveFile makes a copy of the active file in file.orig\n# if the original file does not exist the NO_ORIG flag is set\n#\tto allow restoreAciveFile to remove the active file\n#\n# if the backup (.orig file) exists the backup is not updated\n#\n# $1 is the full path/file name to be backed up\n#\n# returns 0 if backup was made, 1 if not\n#\n\nbackupActiveFile ()\n{\n\t# don't do any work if install has already failed\n\tif $installFailed ; then\n\t\treturn 1\n\tfi\n\tlocal activeFile=\"$1\"\n\tlocal origFile=\"$activeFile.orig\"\n\tlocal noOrigFile=\"$activeFile.NO_ORIG\"\n\n\tif [ -e \"$activeFile\" ]; then\n\t\tif ! [ -e \"$origFile\" ]; then\n\t\t\tcp \"$activeFile\" \"$origFile\"\n\t\t\trm -f \"$noOrigFile\"\n\t\t\treturn 0\n\t\telse\n\t\t\treturn 1\n\t\tfi\n\telse\n\t\tif ! [ -e \"$noOrigFile\" ]; then\n\t\t\ttouch \"$noOrigFile\"\n\t\t\treturn 0\n\t\telse\n\t\t\treturn 1\n\t\tfi\t\t\n\tfi\n}\n\n\n# SetupHelper maintains a set of \"restart flags\"\n#\tthat control service restarts and system reboot\n#\tafter the package has been installed / uninstalled\n#\n# some restarts/reboots are based on the directory of the modified file\n# others are based on the actual file itself\n#\n# not all services or specific files are flagged so some work in the setup script may be needed\n#\n# $1 is the full path and name to the modified file\n\nupdateRestartFlags ()\n{\n\t# flag indicating any file update occurred\n\tfilesUpdated=true\n\n\tcase $1 in\n\t\t/opt/victronenergy/gui*)\n\t\t\trestartGui=true\n\t\t\treturn;;\n\t\t#### TODO: add gui-v2\n\t\t/opt/victronenergy/dbus-generator*/*)\n\t\t\trestartGeneratorService=true\n\t\t\treturn;;\n\t\t/opt/victronenergy/dbus-systemcalc-py/*)\n\t\t\trestartSystemCalc=true\n\t\t\treturn;;\n\t\t/opt/victronenergy/dbus-digitalinputs/*)\n\t\t\trestartDigitalinputs=true\n\t\t\treturn;;\n\n\t\t/u-boot/overlay./*) # Raspberry PI DT overlay\n\t\t\trebootNeeded=true\n\t\t\treturn;;\n\t\t/etc/udev/rules.d/*) # udev rules directory\n\t\t\trebootNeeded=true\n\t\t\treturn;;\n\tesac\n\n\t# reboots based on specific file\n\tcase $( basename $1 ) in\n\t\tgpio_list)\n\t\t\trebootNeeded=true\n\t\t\treturn;;\n\t\tconfig.txt)\n\t\t\trebootNeeded=true\n\t\t\treturn;;\n\tesac\n}\n\n\n# updateActiveFile first backs up the active file\n# then copies the replacement (aka source) to the active file location (aka destination)\n#\n# two variations:\n#\n# updateActiveFile activeFile\n#   an attempt is made to locate the source (replacement)\n#   in the version directory or FileSets\n#\n# updateActiveFile sourceFile activeFile\n#   a separate source (replacement) file is specified\n#\n# both sourceFile and activeFile must be a full path to the file\n#\n# if the update fails, scriptAction is changed to UNINSTALL\n#\n# global thisFileUpdated is set to true if file was updated, false if not\n#\tthisFileUpdated supports the old mechanism which may be used in some setup scripts\n# returns 0 if file was updated, 1 if not\n\n\nupdateActiveFile ()\n{\n    thisFileUpdated=false\n\n\t# don't do any work if install has already failed\n\tif $installFailed ; then\n\t\treturn 1\n\tfi\n\n    local sourceFile=\"\"\n    local activeFile=\"\"\n\n    # separate source and replacement files specified\n    if [ $# == 2 ]; then\n        if [ -f \"$1\" ]; then\n\t\t\tsourceFile=\"$1\"\n        else\n            setInstallFailed $EXIT_FILE_SET_ERROR \"specified soure file \"$1\" does not exist\"\n\t\t\treturn 1\n        fi\n        activeFile=\"$2\"\n    # use active file for both source and destination\n    else\n        activeFile=\"$1\"\n    fi\n\tlocal baseName=$(basename \"$activeFile\")\n\n\t# replacement files are not needed for some versions\n\t# if so marked, leave original untouched\n\tif [ -e \"$fileSet/$baseName.USE_ORIGINAL\" ]; then\n\t\treturn 1\n\tfi\n\n\t# the location of the active file must exist\n\tif [ ! -e $(dirname \"$activeFile\") ]; then\n\t\tlogMessage \"path to $activeFile does not exist - skiping update\"\n\t\treturn 1\n\tfi\n\n    local usePatchedFile=false\n\tlocal patchedReplacement=\"$tempFileDir/$baseName.patchedForInstall\"\n\tlocal currentPatchFile=\"$tempFileDir/$baseName.currentPatch\"\n\n\t# source file not specified separately - look for it in expected places\n    if [ -z \"$sourceFile\" ]; then\n\t\t# first in temp files - patched file\n \t\tif [ -e \"$patchedReplacement\" ] && [ -e \"$currentPatchFile\" ]; then\n            sourceFile=\"$patchedReplacement\"\n            usePatchedFile=true\n\t\t# then in temp files - replacement\n\t\telif [ -e \"$tempFileDir/$baseName\" ]; then\n            sourceFile=\"$tempFileDir/$baseName\"\n\t\t# then in version-specific FileSet\n        elif [ -e \"$fileSet/$baseName\" ]; then\n            sourceFile=\"$fileSet/$baseName\"\n\t\t# then in version-independent file set\n        elif [ -e \"$versionIndependentFileSet/$baseName\" ]; then\n            sourceFile=\"$versionIndependentFileSet/$baseName\"\n\t\t# then in FileSets (previous location of version-indepencent files)\n        elif [ -e \"$pkgFileSets/$baseName\" ]; then\n            sourceFile=\"$pkgFileSets/$baseName\"\n\t\telse\n\t\t\t# if directory for file exists but no sourceFile - can't continue\n\t\t\tif ! [ -e $( dirname \"$activeFile\" ) ]; then\n\t\t\t\tlogMessage \"enclosing directory for $activeFile not found - skipping update\"\n\t\t\telif ! [ -e \"$activeFile\" ]; then\n\t\t\t\tlogMessage \"no replacement or active file $activeFile - skipping update\"\n\t\t\telse\n\t\t\t\tsetInstallFailed $EXIT_FILE_SET_ERROR \"no soure file for $activeFile\"\n\t\t\tfi\n\t\t\treturn 1\n        fi\n    fi\n\n\t# can't continue if other packages modified this file and this is a replacement (not a patch)\n\tlocal local packageList=\"$activeFile.package\"\n    local previousPackage=\"\"\n    local matchFound=false\n\tif [ -e \"$packageList\" ]; then\n\t\tpreviousPackages=$( cat \"$packageList\" )\n\t\tfor previousPackage in ${previousPackages[@]}; do\n\t\t\tif [ \"$packageName\" == \"$previousPackage\" ]; then\n\t\t\t\tmatchFound=true\n\t\t\telif ! $usePatchedFile ; then\n\t\t\t\tsetInstallFailed $EXIT_PACKAGE_CONFLICT \"$baseName was already modfied by $previousPackage\"\n\t\t\t\treturn 1\n\t\t\tfi\n\t\tdone\n\tfi\n\n\t# add file to installed files list (used by uninstallAll)\n\tif ! [ -e \"$installedFilesList\" ] \\\n\t\t\t|| (( $( grep -c \"$activeFile\" \"$installedFilesList\" ) == 0 )); then\n\t\techo \"$activeFile\" >> \"$installedFilesList\"\n\tfi\n\n\t# save the current patch file for use during a future uninstall\n\tlocal previousPatchFile=\"$previousPatchesDir/$baseName.patch\"\n\tif $usePatchedFile ; then\n\t\tcp \"$currentPatchFile\" \"$previousPatchFile\"\n\t# no patch file used for this update\n\telse\n\t\trm -f \"$previousPatchFile\"\n\tfi\n\n\t# if replacement, replace the .package file\n\tif ! $usePatchedFile ; then\n\t\techo $packageName > \"$packageList\"\n\t# if patch and this package not in list yet, add add it\n\telif ! $matchFound ; then\n\t\techo $packageName >> \"$packageList\"\n\tfi\n\n\t# update the active file if needed\n\t# patched files have already incorporated current active file content\n\t# \tso nothing diffrerent here\n\tif ! [ -f \"$activeFile\" ] || ! cmp -s \"$sourceFile\" \"$activeFile\" ; then\n\t\tbackupActiveFile \"$activeFile\"\n\t\tcp \"$sourceFile\" \"$activeFile\"\n\t\tupdateRestartFlags \"$activeFile\"\n\t\tthisFileUpdated=true\n\tfi\n\n\t# insure active file has proper permissions\n\tif [ -f \"$activeFile\" ]; then\n\t\tchmod +r \"$activeFile\"\n\t\tif [[ -x \"$activeFile.orig\" ]] || [[ -x \"$sourceFile\" ]] ; then\n\t\t\tchmod +x \"$activeFile\"\n\t\tfi\n\tfi\n\n\tif $thisFileUpdated; then\n\t\treturn 0\n\telse\n\t\treturn 1\n\tfi\n} # end updateActiveFile ()\n\n\n# restoreActiveFile \n# restores the active file to the content before this package modified it\n# for replacements, the backup copy is moved to the active location\n# if the backup copy doesn't exist BUT the NO_ORIG flag is set\n# the active copy is deleted to restore the system to stock\n#\n# for patches, this package's changes are removed from the active file\n#\tby reverse patching it\n#\n# if the reverse patch fails, the original is restored\n#\tmodifications from all other packages are also removed !!\n#\tthis is drastic but has best chance to leave the system without errors\n#\n# $1 is the active name, the one to be backed up\n#\n# returns 0 if active file was restored, 1 if not\n# also sets thisFileUpdated for backwards compatibility with existing setup scripts\n#\n\nrestoreActiveFile ()\n{\n    thisFileUpdated=false\n\tlocal activeFile=\"$1\"\n\tlocal packageList=\"$activeFile.package\"\n\t# look for this package's name in .package list\n\t#\tand remove it if found\n\tmatchFound=false\n\tremainingPackages=\"\"\n\tif [ -f \"$packageList\" ]; then\n\t\tpreviousPackages=($( cat \"$packageList\" ))\n\t\tfor previousPackage in ${previousPackages[@]}; do\n\t\t\tif [ \"$packageName\" == \"$previousPackage\" ]; then\n\t\t\t\tmatchFound=true\n\t\t\telse\n\t\t\t\tremainingPackages+=\"$previousPackage \"\n\t\t\tfi\n\t\tdone\n\t# no .package file - so proceed with removal anyway\n\telse\n\t\tmatchFound=true\n\tfi\n\t\n\t# if this package not found - nothing to do\n\tif ! $matchFound ; then\n\t\treturn 1\n    fi\n\n\tlocal baseName=$( basename \"$activeFile\" )\n\tlocal previousPatchFile=\"$previousPatchesDir/$baseName.patch\"\n\n\treversePatchError=false\n\trestoreOriginal=false\n\t# no other packages so restore the original\n\tif [ -z \"$remainingPackages\" ] ; then\n\t\trestoreOriginal=true\n\t# other packages have also modified the active file\n\t# attempt to reverse patch the active file\n\t#\tif success, move result into the active position\n\t#\tDO NOT restore original\n\telse\n\t\tif [ -e \"$previousPatchFile\" ]; then\n\t\t\ttempFile=\"$tempFileDir/$baseName\"\n\t\t\tcp \"$activeFile\" \"$tempFile\"\n\t\t\tif $patch --reverse -o \"$activeFile.tmp\" \"$tempFile\" \"$previousPatchFile\" &> /dev/null ; then\n\t\t\t\tmv -f \"$activeFile.tmp\" \"$activeFile\"\n\t\t\t\tthisFileUpdated=true\n\t\t\telse\n\t\t\t\tlogMessage \"CRITICAL: $packageName $baseName - reverse patch failed\"\n\t\t\t\trm -f \"$activeFile.tmp\"\n\t\t\t\treversePatchError=true\n\t\t\tfi\n\t\telse\n\t\t\tlogMessage \"CRITICAL: $packageName $baseName - no prevoius patch file\"\n\t\t\treversePatchError=true\n\t\tfi\n\t\t# if the reverse patch failed, remove mods from ALL packages\n\t\t#### TODO: DRASTIC but can't think of a way around this\n\t\tif $reversePatchError ; then\n\t\t\tmessage1=\"CRITICAL: $package $baseName could not be uninstalled cleanly\"\n\t\t\tmessage2=\"  $remainingPackages must be uninstalled and reinstalled\"\n\t\t\tlogMessage \"$message1\"\n\t\t\tlogMessage \"$message2\"\n\t\t\techo \"$message1\" >> \"$scriptDir/patchErrors\"\n\t\t\techo \"$message2\" >> \"$scriptDir/patchErrors\"\n\n\t\t\t# report errors to other packages as well\n\t\t\tfor otherPackage in $remainingPackages ; do\n\t\t\t\totherPkgPatchErrors=\"$packageRoot/$otherPackage/patchErrors\"\n\t\t\t\techo \"$message1\" >> \"$otherPkgPatchErrors\"\n\t\t\t\techo \"$otherPackage must be uninstalled and reinstalled\" >> \"$otherPkgPatchErrors\"\n\t\t\t\t# remove previous patch in other package(s) since it no longer applies\n\t\t\t\trm -f \"$previousPatchesRoot/$otherPackage/$baseName.patch\"\n\t\t\tdone\n\t\t\tsetInstallFailed $EXIT_PATCH_ERROR \"patch error details were saved in $packageName/patchErrors\"\n\t\tfi\n\tfi\n\n\t# always remove previous patch file\n\trm -f \"$previousPatchFile\"\n\n\t# restore original if no other packages have modified this active file\n\t#\tor if reverse patch failed\n\tif $restoreOriginal || $reversePatchError ; then\n\t\tif [ -e \"$activeFile.orig\" ]; then\n\t\t\tmv -f \"$activeFile.orig\" \"$activeFile\"\n\t\t\tthisFileUpdated=true\n\t\telif [ -f \"$activeFile.NO_ORIG\" ]; then\n\t\t\trm -f \"$activeFile\"\n\t\t\trm -f \"$activeFile.NO_ORIG\"\n\t\t\tthisFileUpdated=true\n\t\tfi\n\n\t\trm -f \"$packageList\"\n\n\t# there are other packages, remove only this package from list\n\telse\n\t\tgrep -v \"$packageName\" \"$packageList\" | tee \"$packageList\" > /dev/null\n\tfi\n\n\t# remove file from installed file list\n\tif [ -f \"$installedFilesList\" ]; then\n\t\tgrep -v \"$activeFile\" \"$installedFilesList\" | tee \"$installedFilesList\" > /dev/null\n\tfi\n\tif $thisFileUpdated; then\n\t\tupdateRestartFlags \"$activeFile\"\n\t\treturn 0\n\telse\n\t\treturn 1\n\tfi\n} # end restoreActiveFile ()\n\n\n# checkFileSets validates the file sets used to install package modifications\n#\n# If a file set for the current Venus OS version exists, the replacement files in that file set\n#\tare usable as is and no further checks are needed.\n# The COMPLETE flag file indicates that the file set was validated on the computer creating\n#\tthe file sets and all replacement files (or symlinks to other file sets) exist.\n#\tNo checks are needed for a COMPLETE file set.\n# If not, an attempt is made to create a file set for the current Venus OS version\n#   If the new active files for the new version all match another version\n#       the new file set is populated automatically with replacement files from the other version\n#\t\tand may be used with no further action\n#   If not, new file set is marked INCOMPLETE and installation failure information is set\n#\tThe package can not be installed on this Venus OS version\n#\n# Replacement files that have no original specify an \"alternate original\" that is used\n# for version comparisons that locate an appropriate replacement\n\ncheckFileSets ()\n{\n\t# no file sets - nothing to check\n\tif ! [ -e \"$pkgFileSets\" ]; then return; fi\n\n\t# no checks needed if all replacement files exist in the selected file set\n\tif [ -f \"$fileSet/COMPLETE\" ]; then return; fi\n\n\t# sort versionList in reverse version order to make searches faster\n\t# since newer versions will most likely contain the desired files to create a new file set\n\tlocal rawVersionList=($(ls -d \"$pkgFileSets\"/v* 2> /dev/null))\n    local tempList=()\n    local fs\n    local baseName\n    local version\n    local versionNumber\n\tfor fs in ${rawVersionList[@]} ; do\n\t\tversion=$(basename $fs)\n\t\tversionStringToNumber $version\n\t\ttempList+=(\"$version:$versionNumber\")\n\tdone\n\tlocal versionList=( $(echo ${tempList[@]} | tr ' ' '\\n' | sort -t ':' -r -n -k 2 | uniq ) )\n\t\n\t# versioned file sets exist but empty file list\n\tif [ ! -z \"$versionList\" ] && [ -z \"$fileList\" ]; then\n        setInstallFailed $EXIT_FILE_SET_ERROR \"empty file list\"\n\t\ttouch \"$fileSet/INCOMPLETE\"\n\t\trm -f \"$fileSet/COMPLETE\"\n        return\n\t# no versioned file sets - nothing to validate - allow install\n\telif [ -z \"$versionList\" ]; then return; fi\n\n\t# attempt to create a new file set or validate an existing one not marked as COMPLETE\n\n\trm -f \"$fileSet/INCOMPLETE\"\n\n    # attempt to create file set if it doesn't exist\n    if [ ! -d \"$fileSet\" ]; then\n        logMessage \"creating a new file set for $venusVersion\"\n        mkdir \"$fileSet\"\n    fi\n\n    for file in ${fileList[@]} ; do\n        baseName=$(basename \"$file\")\n\n\t\t# version-independent file exists\n\t\tif [ -e \"$versionIndependentFileSet/$baseName\" ] || [ -e \"$pkgFileSets/$baseName\" ]; then\n\t\t\t# no versioned files (only version-independent found) - skip version checks\n\t\t\tif [ -z $( find \"$fileSet\"/v* -name $baseName ) ]; then\n\t\t\t\tcontinue\n\t\t\t# continue with tests if any versioned files exist\n\t\t\telse\n\t\t\t\tlogMessage \"WARNING $baseName versioned file exists - version-independent file will be ignored\"\n\t\t\tfi\n\t\tfi\n\n        # skip checks if replacement file already exists\n        # or if there is no replacement file needed\n        if [ -f \"$fileSet/$baseName\" ] || [ -f \"$fileSet/$baseName.USE_ORIGINAL\" ]; then\n            rm -f \"$fileSet/$baseName.NO_REPLACEMENT\"\n\t\t\tcontinue\n        fi\n\n        local activeFile\n        if [ -f \"$altOrigFileDir/$baseName.ALT_ORIG\" ]; then\n            activeFile=$(cat \"$altOrigFileDir/$baseName.ALT_ORIG\")\n        elif [ -f \"$pkgFileSets/$baseName.ALT_ORIG\" ]; then\n            activeFile=$(cat \"$pkgFileSets/$baseName.ALT_ORIG\")\n\t\telse\n\t\t\tactiveFile=$file\n        fi\n\t\tif ! [ -e $( dirname \"$activeFile\" ) ]; then\n\t\t\tlogMessage \"no parent directory $activeFile - skipping checks\"\n\t\t\tcontinue\n\t\tfi\n\n        # package already installed, use .orig file for comparisons\n        if [ -f \"$activeFile.orig\" ]; then\n            activeFile=\"$activeFile.orig\"\n        fi\n\n        # can't process if no original (aka active) file exists in the file set\n        if [ ! -f \"$activeFile\" ]; then\n            logMessage \"ERROR $venusVersion $baseName no active file\"\n            touch \"$fileSet/$baseName.NO_ACTIVE_FILE\"\n\t\t\ttouch \"$fileSet/INCOMPLETE\"\n            continue\n        fi\n\n        # if an active file exists look for a match in another file set\n        if [ ! -z \"$activeFile\" ]; then\n            matchFound=false\n            for entry in ${versionList[@]}; do\n\t\t\t\totherVersion=$(echo $entry | awk -F ':' '{print $1}')\n\n                # skip this version\n                if [ \"$venusVersion\" = \"$otherVersion\" ]; then\n                    continue\n                fi\n\n                otherFile=\"$pkgFileSets/$otherVersion/$baseName\"\n\n                # skip symbolic links and nonexistent originals\n                if [ ! -f \"$otherFile.orig\" ] || [ -L \"$otherFile.orig\" ] ; then\n                    continue\n                fi\n                \n                # files match\n                if cmp -s \"$activeFile\" \"$otherFile.orig\" > /dev/null ; then\n                    matchFound=true\n                    break\n                fi\n            done\n \n            if $matchFound ;then\n                rm -f \"$fileSet/$baseName.orig\"\n                rm -f \"$fileSet/$baseName.NO_ORIG\"\n                # if other file set contains a replacement file, link to it\n                if [ -f \"$otherFile\" ]; then\n\t\t\t\t\trm -f \"$fileSet/$baseName\"\n                    ln -s \"../$otherVersion/$baseName\" \"$fileSet/$baseName\"\n                    rm -f \"$fileSet/$baseName.NO_REPLACEMENT\"\n                    rm -f \"$fileSet/$baseName.USE_ORIGINAL\"\n                # if other file set does not contain a replacement, this one will not either\n                # this IS permitted and handled in the updateActiveFile and restoreActiveFile functions\n                elif [ -f \"$otherFile.USE_ORIGINAL\" ]; then\n                    touch \"$fileSet/$baseName.USE_ORIGINAL\"\n                    rm -f \"$fileSet/$baseName.NO_REPLACEMENT\"\n                fi\n            # no match to a previous verison - can't create file set automatically\n            # but copy original file to aid manual editing\n            else\n                logMessage \"ERROR $venusVersion $baseName no replacement file\"\n                cp \"$activeFile\" \"$fileSet/$baseName.orig\"\n                touch \"$fileSet/$baseName.NO_REPLACEMENT\"\n                touch \"$fileSet/INCOMPLETE\"\n            fi\n        fi\n    done\n\n    if [ -f \"$fileSet/INCOMPLETE\" ]; then\n        setInstallFailed $EXIT_FILE_SET_ERROR \"incomplete file set for $venusVersion\"\n\t# if we get this far and fs is not marked INCOMPLETE, then the file set does not need to be checked again next pass\n\telse\n\t\ttouch \"$fileSet/COMPLETE\"\n    fi\n}\n\n# builds the installedFilesList and installedServices lists from the package's setup script\n# this is needed for installs with a prior version of SetupHelper that did not\n#\tcreate the installed... lists\n#\n# uninstall... functions then use these lists to uninstall\n\nbuildUninstallListsfromSetupScript ()\n{\n\t# prevent this from running a second time\n\tif [ \"$uninstallListsAlreadyBuilt\" == 'yes' ]; then\n\t\treturn\n\tfi\n\tuninstallListsAlreadyBuilt='yes'\n\tlocal param\n\n\tscriptUninstallFilesList=()\n\tscriptUninstallServicesList=()\n\twhile read -r line || [[ -n \"$line\" ]]; do\n\t\tcommandFound=false\n\t\tread -a params <<< $line\n\t\tnumberOfParams=${#params}\n\t\tfor (( i=0; i < $numberOfParams; i++ ));do\n\t\t\tcase \"${params[i]}\" in\n\t\t\tupdateActiveFile)\n\t\t\t\t# parameter of intrest is the second one if it exists\n\t\t\t\t# otherwise, it should be the first one\n\t\t\t\tparam=${params[i+2]}\n\t\t\t\tif [ -z \"$param\" ] || [[ $param == \\#* ]]; then\n\t\t\t\t\tparam=$( echo ${params[i+1]} | sed s?'$qmlDir'?$qmlDir? )\n\t\t\t\tfi\n\t\t\t\tif ! [ -z \"$param\" ] && ! [[ $param == \\#* ]]; then\n\t\t\t\t\t# remove any  quotes around parameters\n\t\t\t\t\tparam=$( echo $param | sed -e 's?\"?? g' -e \"s?'?? g\" )\n\t\t\t\t\tscriptUninstallFilesList+=(\"$param\")\n\t\t\t\tfi\n\t\t\t\t;;\n\t\t\tinstallService)\n\t\t\t\t# parameter of intrest is the first one if it exists\n\t\t\t\t# otherwise, is the packageName\n\t\t\t\tparam=${params[i+1]}\n\t\t\t\tif ! [ -z \"$param\" ] && ! [[ $param == \\#* ]]; then\n\t\t\t\t\tscriptUninstallServicesList+=(\"$param\")\n\t\t\t\telse\n\t\t\t\t\t# remove any quotes around parameters\n\t\t\t\t\tparam=$( echo $param | sed -e 's?\"?? g' -e \"s?'?? g\" )\n\t\t\t\t\tscriptUninstallServicesList+=($packageName)\n\t\t\t\tfi\n\t\t\t\t;;\n\t\t\tesac\n\t\tdone\n\tdone < \"$scriptDir/setup\"\n}\n\n# install / uninstall all files / services functions\n#\n# the install... functions are called from endScript if the related flags are set\n#\tbut may also be called in the setup script\n#\tif processing needs to be done after the files are installed\n#\n# NOTE: uninstall functions are called in endScrpit during an uninstall\n#\t\t\tthere are no flags to control this !\n#\n# NOTE: only services in the package's serice directory are installed\n#\t\tany services in the package's root diretory are not installed\n\n\ninstallAllFiles ()\n{\n\tif [ -z \"$fileListAll\" ]; then\n\t\tgetFileLists \"$pkgFileSets\"\n\tfi\n\n\tif [ ! -z \"$fileListAll\" ]; then\n\t\tlogMessage \"installing files\"\n\t\tlocal file\n\t\tfor file in ${fileListAll[@]}; do\n\t\t\tif $installFailed ; then break; fi\n\t\t\tupdateActiveFile \"$file\"\n\t\tdone\n\tfi\n}\n\n# uninstall files from\n#\tinstalled files list if present\n#\tand from file lists in the package\n#\tand from updateActiveFile calls found in the setup script\n# this insures complete uninstall even for packages installed\n#\twith SetupHelper prior to v6.0\n\nuninstallAllFiles ()\n{\n\tlocal restoreFilesList=()\n\t# add installdeFilesList if present (might be empty but that's fine)\n\tif [ -f \"$installedFilesList\" ]; then\n\t\trestoreFilesList=( $( cat \"$installedFilesList\" ) )\n\tfi\n\t# add file lists & calls to installActiveFile in setup script\n\trestoreFilesList+=( ${fileListAll[@]} )\n\tbuildUninstallListsfromSetupScript\n\trestoreFilesList+=( ${scriptUninstallFilesList[@]} )\n\n\t# remove duplicates\n\tif (( ${#restoreFilesList[@]} > 1 )); then\n\t\trestoreFilesList=( $(printf \"%s\\n\" \"${restoreFilesList[@]}\" | sort -u ) )\n\tfi\n\n\t# uninstall the files\n\tif [ ! -z \"$restoreFilesList\" ]; then\n\t\tlogMessage \"uninstalling files\"\n\t\tlocal file\n\t\tfor file in ${restoreFilesList[@]}; do\n\t\t\trestoreActiveFile $file\n\t\tdone\n\tfi\n}\n\n\ninstallAllServices ()\n{\n\tlocal serviceList\n\tlocal service\n\t# get list of services in the package's service directory\n\tif [ -d \"$servicesDir\" ]; then\n\t\tservicesList=( $( cd \"$servicesDir\"; ls -d * 2> /dev/null ) )\n\t\tif ! [ -z \"$servicesList\" ]; then\n\t\t\tlogMessage \"installing services\"\n\t\t\tfor service in ${servicesList[@]} ; do\n\t\t\t\tif $installFailed; then break; fi\n\t\t\t\tinstallService $service\n\t\t\tdone\n\t\tfi\n\tfi\n}\n\n\n# uninstal services found in the services directory\n#\tand restoreServices call in the package's setup script\n\nuninstallAllServices ()\n{\n\tlocal tempList=()\n\tlocal servicesList=()\n\tlocal service\n\n\tif [ -f \"$installedServicesList\" ]; then\n\t\tservicesList=( $( cat \"$installedServicesList\" ) )\n\tfi\n\t# add list from the setup script itself\n\tbuildUninstallListsfromSetupScript\n\tservicesList+=( ${scriptUninstallServicesList[@]} )\n\n\t# remove duplicates\n\tif (( ${#servicesList[@]} > 1 )); then\n\t\tservicesList=( $(printf \"%s\\n\" \"${servicesList[@]}\" | sort -u ) )\n\tfi\n\n\t# uninstall services\n\tif [ ! -z \"$servicesList\" ]; then\n\t\tlogMessage \"uninstalling services\"\n\t\tfor service in ${servicesList[@]} ; do\n\t\t\tif [ -z \"$service\" ]; then\n\t\t\t\tremoveService $packageName\n\t\t\telse\n\t\t\t\tremoveService $service\n\t\t\tfi\n\t\tdone\n\tfi\n}\n\n\n# restart the GUI V1 service\n# begining at about v3.20~18, changes were made to accommodate the gui-v2\n# and these changes require different handling of a GUI service restart\n\n\nrestartGuiV1Service ()\n{\n\t# gui is the older service that runs GUI v1 only\n\tif [ -e \"/service/gui\" ]; then\n\t\tlogMessage \"restarting GUI V1 (/service/gui)\"\n\t\tsvc -t \"/service/gui\"\n\t# restart GUI if NOT running v2 or can't determine if GUI v1 or v2 is selected\n\telif [ -e \"/service/start-gui\" ]; then\n\t\tlocal guiVersion=\"$(dbus-send --system --print-reply --dest=com.victronenergy.settings /Settings/Gui/RunningVersion com.victronenergy.BusItem.GetValue | grep variant | awk '{print $3}')\"\n\t\tif (( $guiVersion != 2 )); then\n\t\t\tlogMessage \"restarting GUI V1 (/service/start-gui)\"\n\t\t\tsvc -t \"/service/start-gui\"\n\t\tfi\n\tfi\n}\n\n# for backward compatibility (oler setup scripts)\nrestartGuiService ()\n{\n\trestartGuiV1Service\n}\n\nrestartGuiV2Service ()\n{\n\t# restart GUI if NOT running v1 or can't determine if GUI v1 or v2 is selected\n\tif [ -e \"/service/start-gui\" ]; then\n\t\tlocal guiVersion=\"$(dbus-send --system --print-reply --dest=com.victronenergy.settings /Settings/Gui/RunningVersion com.victronenergy.BusItem.GetValue | grep variant | awk '{print $3}')\"\n\t\tif (( $guiVersion != 1 )); then\n\t\t\tlogMessage \"restarting GUI V2 (/service/start-gui)\"\n\t\t\tsvc -t \"/service/start-gui\"\n\t\tfi\n\tfi\n}\n\n# determine how the setup script should exit based on $scriptAction and other flags\n# services may be restarted here also\n#\n# endScript accepts these optional parameters which determines if files and/or services are installed\n#\n#\t'INSTALL_FILES' causes files from the file lists to be installed\n#\t'INSTALL_SERVICES' causes services in the services directory to be installed\n#\t\tall services must be in the package's services directory\n#\t'ADD_DBUS_SETTINGS' will add/update dBus settings from the DbusSettingsList\n#\tany or may be included\n# do NOT include these if related processing is needed in the setup script !\n# instead, call installAllFiles and/or installAllServices in line with the other processing\n#\tor call updateActiveFile or installService directly\n#\n# this function completes package installation \n#\tand sets up conditions for reinstallation following a Venus Os firmware update\n#\n# may EXIT or REBOOT within the function - DOES NOT RETURN TO CALLER           \n\n\nendScript ()\n{\n    if [ $scriptAction == 'INSTALL' ] && ! $installFailed ; then\n\t\t# do installs as indicated from caller\n\t\twhile (( $# > 0 )); do\n\t\t\tcase \"$1\" in\n\t\t\t\t'INSTALL_FILES')\n\t\t\t\t\tinstallAllFiles\n\t\t\t\t\t;;\n\t\t\t\t'INSTALL_SERVICES')\n\t\t\t\t\tinstallAllServices\n\t\t\t\t\t;;\n\t\t\t\t'ADD_DBUS_SETTINGS')\n\t\t\t\t\taddAllDbusSettings\n\t\t\t\t\t;;\n\t\t\tesac\n\t\t\tshift\n\t\tdone\n\n\t\t# assume that if we get this far, any command line opitons have already been set\n\t\ttouch \"$setupOptionsDir/optionsSet\"\n\t\t\n\t\t# clear flag preventing auto installs in PackageManager\n\t\trm -f \"$setupOptionsDir/DO_NOT_AUTO_INSTALL\"\n\n\t\t# if script needs to run again, installedVersionFile flag file is removed\n\t\t# script should run again at boot time via reinstallMods\n\t\tif $runAgain ; then\n\t\t  logMessage \"script will run again at startup\"\n\t\t\trm -f \"$installedVersionFile\"\n\t\t# otherwise, installation is complete - update installedVersion\n\t\telse\n\t\t\tcp \"$scriptDir/version\" \"$installedVersionFile\"\n\t\tfi\n\n\t\t# update rc.local to include call to reinstallMods\n\t\t#\tdo only for SetupHelper since other packages are now installed by PackageManager\n\t\tif [ \"$packageName\" == \"SetupHelper\" ]; then\n\t\t\tif [ ! -f \"$rcLocal\" ]; then\n\t\t\t\tlogMessage \"creating $rcLocal\"\n\t\t\t\tcp \"$scriptDir/rcS.local\" \"$rcLocal\"\n\t\t\t\tchmod +x \"$rcLocal\"\n\t\t\telif  [ $(grep -c \"blind install\" \"$rcLocal\") -gt 0 ]; then\n\t\t\t\tlogMessage \"REPLACING blind install $rcLocal with the standard one\"\n\t\t\t\trm -f \"$rcLocal\"\n\t\t\t\tcp \"$scriptDir/rcS.local\" \"$rcLocal\"\n\t\t\t\tchmod +x \"$rcLocal\"\n\t\t\telif  [ $(grep -c \"SetupHelper\" \"$rcLocal\") == 0 ]; then\n\t\t\t\tlogMessage \"adding SetupHelper reinstall script to $rcLocal\"\n\t\t\t\tsed -e '1,2d' \"$scriptDir/rcS.local\" >> $rcLocal\n\t\t\tfi\n\t\tfi\n\tfi\n\t\t\n\n    if [ $scriptAction == 'UNINSTALL' ] ; then\n\t\tif $installFailed ; then\n\t\t\tlogMessage \"INSTALL failed - attempting UNINSTALL in endScript\"\n\t\t# package was actually uninstalled (not an install failure) - set flag preventing auto installs\n\t\telse \n\t\t\ttouch \"$setupOptionsDir/DO_NOT_AUTO_INSTALL\"\n\t\tfi\n\n\t\t# ALWAYS attempt to uninstall files and services\n\t\tuninstallAllFiles\n\t\tuninstallAllServices\n\n        # flag package not installed since package is being removed\n        rm -f \"$installedVersionFile\"\n\n\t\t# when uninstalling SetupHelper, remove EMPTY installed...Lists in all packages\n\t\t#\tto prevent a future install with an older SetupHelper\n\t\t#\tconfusing a future future uninstall with a new SetupHelper\n\t\tif [ \"$packageName\" == \"SetupHelper\" ] && [ -e \"$installedFilesDir\" ]; then\n\t\t\tfor file in $(ls \"$installedFilesDir\") ; do\n\t\t\t\tif ! [ -s \"$installedFilesDir/$file\" ]; then\n\t\t\t\t\trm \"$installedFilesDir/$file\"\n\t\t\t\tfi\n\t\t\tdone\n\t\t\t# remove lines from rcS.local\n\t\t\tsed -i -e \"/# SetupHelper reinstall/,/fi/d\" \"$rcLocal\"\n\t\tfi\n\tfi\n\n\t# setup script signals nothing to do - exit without further action without errors\n\tif ! $installFailed && ! $uninstallFailed; then\n\t\tif [ $scriptAction == 'EXIT' ]; then\n\t\t\texit $EXIT_SUCCESS\n\t\telif ! [ $scriptAction == 'INSTALL' ] && ! [ $scriptAction == 'UNINSTALL' ]; then\n\t\t\tsetInstallFailed $EXIT_ERROR \"unexpected script action $scriptAction - did not install or uninstall\"\n\t\t\texit $EXIT_ERROR\n\t\tfi\n\tfi\n\n\t# check for reboot and service restarts\n\tif $rebootNeeded ; then\n\t\tif $userInteraction ; then\n\t\t\tif yesNoPrompt \"Reboot system now (y) or do it manually later (n): \" ; then\n\t\t\t\tdeferReboot=false\n\t\t\telse\n\t\t\t\tlogMessage \"system must be rebooted to finish installation and activate components\"\n\t\t\t\tdeferReboot=true\n\t\t\tfi\n\t\tfi\n\tfi\n\n\t# restart services if a reboot won't happen below\n\tif ! $rebootNeeded || $deferReboot ; then\n\t\tif $restartGeneratorService ; then\n\t\t\tlogMessage \"restarting generator service\"\n\t\t\tif [ -e \"svc -t /service/dbus-generator\" ]; then\n\t\t\t\tsvc -t /service/dbus-generator\n\t\t\tfi\n\t\t\tif [ -e \"svc -t /service/dbus-generator-starter\" ]; then\n\t\t\t\tsvc -t /service/dbus-generator-starter\n\t\t\tfi\n\t\tfi\n\t\tif $restartSystemCalc ; then\n\t\t\tlogMessage \"restarting systemcalc service\"\n\t\t\tsvc -t /service/dbus-systemcalc-py\n\t\tfi\n\t\tif $restartDigitalinputs ; then\n\t\t\tlogMessage \"restarting digital inputs service\"\n\t\t\tsvc -t /service/dbus-digitalinputs\n\t\tfi\n\tfi\n\t#### TODO: add gui v2\n\t# restart GUI if not doing a reboot below\n\tif $restartGui && ! $rebootNeeded && ! $deferReboot; then\n\t\tif $userInteraction ; then\n\t\t\tif yesNoPrompt \"Restart the GUI now (y) or issue a do it manually later (n): \" ; then\n\t\t\t\trestartGuiV1Service\n\t\t\t\tdeferGuiRestart=false\n\t\t\t\trestartGui=false\n\t\t\tfi\n\t\telse\n\t\t\t# GUI restart NOT deferred - do it now\n\t\t\tif ! $deferGuiRestart ; then\n\t\t\t\trestartGuiV1Service\n\t\t\t\tdeferGuiRestart=false\n\t\t\t\trestartGui=false\n\t\t\tfi\n\t\tfi\n\tfi\n\n\tlocal exitCode=$EXIT_SUCCESS\n\tif $installFailed ; then\n\t\tif [ $scriptAction == 'UNINSTALL' ]; then\n\t\t\tif $uninstallFailed ; then\n\t\t\t\tlogMessage \"CRITICAL: install failed with error $installExitReason\"\n\t\t\t\tlogMessage \"CRITICAL: uninstall also failed with error $uninstallExitReason - package state unknown\"\n\t\t\t\texitCode=$uninstallExitReason\n\t\t\telse\n\t\t\t\tlogMessage \"install failed - package has been successfully uninstalled\"\n\t\t\t\texitCode=$installExitReason\n\t\t\tfi\n\t\telse\n\t\t\tlogMessage \"install failed during prechecks - no changes were made\"\n\t\t\texitCode=$installExitReason\n\t\tfi\n\telif $uninstallFailed ; then\n\t\tlogMessage \"CRITICAL: uninstall failed - package state unknown\"\n\t\texitCode=$installExitReason\n\telif $rebootNeeded ; then\n\t\tif $deferReboot ; then\n\t\t\tlogMessage \"reboot needed to complete operaiton\"\n\t\t\texitCode=$EXIT_REBOOT\n\t\telse\n\t\t\tlogMessage \"rebooting ...\"\n\t\t\treboot\n\t\tfi\n\telif $restartGui && $deferGuiRestart ; then\n\t\techo \"GUI must be restarted to activate changes\"\n\t\texitCode=$EXIT_RESTART_GUI\n\t# install/uninstall succeeded\n\telse\n\t\tlogMessage \"complete - no errors\"\n\t\texitCode=$EXIT_SUCCESS\n\tfi\n\n\texit $exitCode\n} # endScript ()\n\n######## this code is executed in-line when CommonResources is sourced\n\n# check for reinstall parameter\n# set $scriptAction to control work following the source command\n# if \"force\" is also provided on the command line, then the installedVersionFile is not checked\n# installedVersionFile contains the installed version (if any)\n# it is compared to the version file in the package directory\n#  if installedVersionFile is missing or contents are different, the installation will proceed\n# if the two versions match, there is no need to reinstall the package\n# we assume a reinstall is always run without benefit of a console (runningAtBoot will be true)\n# so there will be no prompts and all actions will be automatic\n#\n# \"deferReboot\" signals that endScript should not reboot the system, but return EXIT_REBOOT\n#\tassuming the caller will evenutally reboot the system\n#\n# \"deferGuiRestart\" is similar for restarting the GUI\n#\n# \"install\" causes the package to be installed silently\n# \"uninstall\" causes the package to be uninstalled silently\n#\n# command line parameters may appear in any order\n#\n#\n# logToConsole is set to true in the LogHandler script\n# It is set to false here the 'auto' parameter is passed on the command line\n#\twhich indicates this script is NOT being run from the command line\n\n# cleanup from previous versions - reinstallScriptsList no loner used\nrm -f \"/data/reinstallScriptsList\"\n\n# initialize version strings and numbers for future checks\nif [ -f \"$installedVersionFile\" ]; then\n\tinstalledVersion=$(cat \"$installedVersionFile\")\n\tversionStringToNumber $installedVersion\n\tinstalledVersionNumber=$versionNumber\nelse\n\tinstalledVersion=\"\"\n\tinstalledVersionNumber=0\nfi\n\npackageVersionFile=\"$scriptDir/version\"\nif [ -f \"$packageVersionFile\" ]; then\n\tpackageVersion=$(cat \"$packageVersionFile\")\n\n\tversionStringToNumber $packageVersion\n\tpackageVersionNumber=$versionNumber\nelse\n\tpackageVersion=\"\"\n\tpackageVersionNumber=0\nfi\n\n# collect command line options\nreinstall=false\ndeferReboot=false\ndeferGuiRestart=false\nuserInteraction=true\nrunFromPm=false\nwhile [ $# -gt 0 ]; do\n\tcase $1 in\n\t\t\"reinstall\")\n\t\t\treinstall=true\n\t\t\t;;\n\t\t\"deferReboot\")\n\t\t\tdeferReboot=true\n\t\t\t;;\n\t\t\"deferGuiRestart\")\n\t\t\tdeferGuiRestart=true\n\t\t\t;;\n\t\t\"install\")\n\t\t\tscriptAction='INSTALL'\n\t\t\t;;\n\t\t\"uninstall\")\n\t\t\tscriptAction='UNINSTALL'\n\t\t\t;;\n\t\t\"auto\")\n\t\t\tlogToConsole=false\n\t\t\tuserInteraction=false\n\t\t\t;;\n\t\t\"runFromPm\")\n\t\t\trunFromPm=true\n\t\t\tlogToConsole=false \n\t\t\tuserInteraction=false\n\t\t\tdeferReboot=true\n\t\t\tdeferGuiRestart=true\n\t\t\t;;\n\t\t\"check\")\n\t\t\t# if no other actions were set, set it here\n\t\t\t#\tbut allow other actions to override this one\n\t\t\tif [ $scriptAction == 'NONE' ]; then\n\t\t\t\tscriptAction='CHECK'\n\t\t\tfi\n\t\t\t;;\n\t\t*)\n\tesac\n    shift\ndone\n\n# do after logToConsole is enabled/disabled abvove\nlogMessage \"--- starting setup script $packageVersion action: $scriptAction\"\nif ! $runFromPm && $logToConsole ; then\n\techo\n\techo \"messages are NOT written to log file - only to console\"\n\techo\nfi\n\n# packages that require options to proceed unattended\n# must include the optionsRequried flag file in their package directory\n# if the flag is present and options haven't been previously set,\n#\tSD/USB media will be checked for the package options directory\n#\tand copy them into position\n\nopitonsRequiredFile=\"$scriptDir/optionsRequired\"\noptionsSet=false\nif [ -f $opitonsRequiredFile ]; then\n\tif [ -f \"$setupOptionsDir/optionsSet\" ]; then\n\t\toptionsSet=true\n\t# options not set - check media for options if doing a blind install\n\telif [ $scriptAction == 'INSTALL' ]; then\n\t\tmediaList=($(ls /media))\n\t\tfor dir in ${mediaList[@]} ; do\n\t\t\taltSetupDir=\"/media/$dir/\"$(basename $setupOptionsRoot)\"/$packageName\"\n\t\t\tif [ -f \"$altSetupDir/optionsSet\" ]; then\n\t\t\t\tcp -r \"$altSetupDir\" \"$setupOptionsRoot\"\n\t\t\t\tif [ -f \"$setupOptionsDir/optionsSet\" ]; then\n\t\t\t\t\tlogMessage \"options retrieved from SD/USB media\"\n\t\t\t\t\toptionsSet=true\n\t\t\t\tfi\n\t\t\t\tbreak\n\t\t\tfi\n\t\tdone\n\tfi\n\n# no command line options are needed - ok to reinstall even if\n#\tsetup was not run from the command line\nelse\n\toptionsSet=true\nfi\n\n# called from reinstallMods at boot time\nif $reinstall ; then\n    runningAtBoot=true\n\t# not installed, do it now\n    if (( installedVersionNumber == 0 )); then\n        scriptAction='INSTALL'\n\t# check versions and install only if package version is newer than installed version\n\telse\n\t\t# trigger install if version numbers differ\n\t\tif (( installedVersionNumber != packageVersionNumber )); then\n\t\t\tscriptAction='INSTALL'\n\t\telse\n\t\t\texit $EXIT_SUCCESS\n\t\tfi\n\tfi\n\n# not running from reinstallMods\nelse\n    runningAtBoot=false\nfi\n\nif [ ! -d \"$setupOptionsDir\" ]; then\n\tlogMessage \"creating package options directory $setupOptionsDir\"\n\tmkdir -p $setupOptionsDir\nfi\n\n# initialze integer version number for venus version\n# used below and in checkFileSets\nversionStringToNumber $venusVersion\nvenusVersionNumber=$versionNumber\n\ngetFileLists \"$pkgFileSets\"\n\n# create temporary directory for temporary install/uninstall files (unique temp directory in volatile storage)\n#\tupdateActiveFile always checks this location first for a replacement before checking file sets\n# tempFileDir is removed in the exit trap above but it is in volatile storage so will be removed on boot anyway\ntempFileDir=$( mktemp -d )\n\n# patch files previously used to patch a file are stored in /etc/venus\n# so they track the selected root fs and are erased when Venus OS is updated\npreviousPatchesRoot=\"/etc/venus/previousPatches\"\npreviousPatchesDir=\"$previousPatchesRoot/$packageName\"\n\n# do install pre-checks - skip if uninstalling\nif [ $scriptAction != 'UNINSTALL' ]; then\n\n\t# prevent installing Raspberry Pi packages on other platforms\n\tif [ -f \"$scriptDir/raspberryPiOnly\" ]; then \n\t\tif [[ $machine != *\"raspberrypi\"* ]]; then\n\t\t\tsetInstallFailed $EXIT_INCOMPATIBLE_PLATFORM \"$packageName not compatible with $machine\"\n\t\tfi\n\tfi\n\t# check to see if package is compatible with this Venus version\n\tif [ -f \"$scriptDir/firstCompatibleVersion\" ]; then\n\t\tfirstCompatibleVersion=$(cat \"$scriptDir/firstCompatibleVersion\")\n\t# no first compatible version specified - use the default\n\telse\n\t\tfirstCompatibleVersion='v3.10'\n\tfi\n\tif [ -f \"$scriptDir/obsoleteVersion\" ]; then\n\t\tobsoleteVersion=$(cat \"$scriptDir/obsoleteVersion\")\n\t\tversionStringToNumber $obsoleteVersion\n\t\tobsoleteVersionNumber=$versionNumber\n\telse\n\t\tobsoleteVersionNumber=9999999999999999\n\tfi\n\n\tversionStringToNumber $firstCompatibleVersion\n\tfirstCompatibleVersionNumber=$versionNumber\n\tif (( $venusVersionNumber < $firstCompatibleVersionNumber )); then\n\t\tsetInstallFailed $EXIT_INCOMPATIBLE_VERSION \"$venusVersion before first compatible $firstCompatibleVersion\"\n\telif (( $venusVersionNumber >= $obsoleteVersionNumber )); then\n\t\t\tsetInstallFailed $EXIT_INCOMPATIBLE_VERSION \"$venusVersion after last compatible $obsoleteVersion\"\n\t# validate firmware version if valid firmware version file exists\n\telif [ -e \"$scriptDir/validFirmwareVersions\" ]; then\n\t\tincompatibleVersion=false\n\t\tif ! grep -xq \"$venusVersion\" \"$scriptDir/validFirmwareVersions\" ; then\n\t\t\tincompatibleVersion=true\n\t\tfi\n\t\t# user can override this from command line (for testing new versions mainly)\n\t\tif $incompatibleVersion && $userInteraction ; then\n\t\t\tif yesNoPrompt \"$venusVersion not in the valid firmware list proceed any way (y/n)? \" ; then \n\t\t\t\tincompatibleVersion=false\n\t\t\tfi\n\t\tfi\n\t\tif $incompatibleVersion ; then\n\t\t\tsetInstallFailed $EXIT_INCOMPATIBLE_VERSION \"$venusVersion not in valid firmware list\"\n\t\tfi\n\tfi\n\n\t# determine if GUI v1 is installed\n\t#\tNote, it may NOT be running or selected to run!!!!\n\tif [ ! -d \"/opt/victronenergy/gui\" ]; then\n\t\tguiV1present=false\n\telse\n\t\tguiV1present=true\n\tfi\n\t# block installs if any GUI files would be modified and GUI v1 is not present\n\t# packages can bypass GUI v1 checks and allow installs even if package contains them\n\tif [ -f \"$scriptDir/GUI_V1_NOT_REQUIRED\" ]; then\n\t\tguiV1required=false\n\t# files in the GUI v1 directory are considered mandatory\n\telif (( $( cat \"$pkgFileSets/fileList\"* 2>/dev/null | grep -c '/gui/' ) > 0 )); then\n\t\tguiV1required=true\n\t# look also in setup script\n\telif (( $( grep 'updateActiveFile' \"$scriptDir/setup\" | grep -c '$qmlDir\\|/gui/') > 0 )); then\n\t\tguiV1required=true\n\telse\n\t\tguiV1required=false\n\tfi\n\tif ! $guiV1present && $guiV1required ; then\n\t\tsetInstallFailed $EXIT_NO_GUI_V1 \"$packageName requires GUI v1\"\n\tfi\n\n\t# attempting an install without the comand line prompting\n\t#\tand needed options have not been set yet - can't continue\n\tif ! $installFailed && [ $scriptAction == 'INSTALL' ]; then\n\t\tif ! $optionsSet ; then\n\t\t\tsetInstallFailed $EXIT_OPTIONS_NOT_SET \"required options have not been set\"\n\t\tfi\n\tfi\n\n\tcheckFileSets\n\n\t# checkFileSets created a missing file set and set the INCOMPLETE flag if needed\n\t# that is all CHECK needed to do so EXIT HERE !!!!\n\tif [ $scriptAction == 'CHECK' ]; then\n\t\tif [ -f \"$fileSet/INCOMPLETE\" ]; then\n\t\t\texit $EXIT_FILE_SET_ERROR\n\t\tfi\n\tfi\n\n\tcheckPackageDependencies\n\n\t# create patched files for files with VisibleItemModel for older Venus OS versions\n\tif ! $installFailed ; then\n\t\tversionStringToNumber \"v3.00~14\"\n\t\tif (( $venusVersionNumber < $versionNumber )); then\n\t\t\tlogMessage \"patching VisibleItemModel to VisualItemModel in all .qml replacements\"\n\t\t\tfor file in ${fileListVersionIndependent[@]};  do\n\t\t\t\tbaseName=$( basename \"$file\" )\n\t\t\t\tif ! [[ \"$baseName\" == *.qml ]]; then continue; fi\n\t\t\t\tsourceFile=\"$versionIndependentFileSet/$baseName\"\n\t\t\t\tif ! [ -f \"$sourceFile\" ]; then continue; fi\n\t\t\t\tif (( $(grep -c \"VisibleItemModel\" \"$sourceFile\") == 0 )); then continue; fi\n\t\t\t\tsed -e 's/VisibleItemModel/VisualItemModel/' \"$sourceFile\" > \"$tempFileDir/$baseName\"\n\t\t\tdone\n\t\tfi\n\n\t\t# create patched files for all qml files for the change to QtQuick 2\n\t\tversionStringToNumber \"v3.60~18\"\n\t\tif (( $venusVersionNumber >= $versionNumber )); then\n\t\t\tlogMessage \"changing QtQuick 1.1 to QtQuick 2 in all .qml replacements\"\n\t\t\tfor file in ${fileListVersionIndependent[@]};  do\n\t\t\t\tbaseName=$( basename \"$file\" )\n\t\t\t\tif ! [[ \"$baseName\" == *.qml ]]; then continue; fi\n\t\t\t\tsourceFile=\"$versionIndependentFileSet/$baseName\"\n\t\t\t\tif ! [ -f \"$sourceFile\" ]; then continue; fi\n\t\t\t\tif (( $(grep -c \"QtQuick 1.1\" \"$sourceFile\") == 0 )); then continue; fi\n\t\t\t\tsed -e 's/QtQuick 1.1/QtQuick 2/' \"$sourceFile\" > \"$tempFileDir/$baseName\"\n\t\t\tdone\n\t\tfi\n\tfi\n\n\t# create the forward and reverse patched files\n\t# used during the actual install and to test if the patch/reverse patch will succeed\n\t# done here so PackageManager knows if this will be possible before starting the install\n\t#\n\t# if this and other packages have both modified the active file,\n\t#\tthe patch from this package is first removed\n\t#\t\tby reverse patching the active file with the PREVIOUS patch file\n\t#\tthe new patch is then applied\n\t#\ta test reverse patch insures the patch can be removed in the future\n\t#\tthe patch file used for this patch is then saved so it can be used\n\t#\t\tfor the reverse patch on next install/uninstall\n\t#\n\t# if no other packages have modified the active file,\n\t#\tthe patch is applied to .orig file if exists\n\t#\trather than reverse patching the active file\n\t#\tthis maintains compatibility with packages installed with older versions of SetupHelper\n\n\t# use local patch executable - BusyBox version has bugs and does not support all options\n\t# include options that are used in all calls\n\t# RPI 5 builds use a different binary format so choose the version that works\n\tif $( /data/SetupHelper/patch -v &> /dev/null ); then\n\t\tpatch=\"/data/SetupHelper/patch --force --silent --reject-file=/dev/null\"\n\telif $( /data/SetupHelper/patchBookworm -v &> /dev/null ); then\n\t\tpatch=\"/data/SetupHelper/patchBookworm --force --silent --reject-file=/dev/null\"\n\telse\n\t\tpatch=\"\"\n\t\techo \"patch executable incompatible with this OS version\" > \"$scriptDir/patchErrors\"\n\t\tsetInstallFailed $EXIT_PATCH_ERROR \"patch executable incompatible with this OS version - can't continue\"\n\t\t# pre-checks only - direct exit\n\t\tif [ $scriptAction == 'CHECK' ]; then\n\t\t\texit $EXIT_PATCH_ERROR\n\t\t# script will exit since we are still in pre-checks !!\n\t\telse\n\t\t\tendScript\n\t\tfi\n\tfi\n\n\tif ! $installFailed && ! [ -z \"$fileListPatched\" ]; then\n\t\tpatchErrors=()\n\t\tfor activeFile in ${fileListPatched[@]}; do\n\t\t\tbaseName=$( basename $activeFile )\n\t\t\ttempActiveFile=\"$tempFileDir/$baseName.tmp\"\n\t\t\tforwardPatched=\"$tempFileDir/$baseName.patchedForInstall\"\n\t\t\tcurrentPatchFile=\"$tempFileDir/$baseName.currentPatch\"\n\t\t\tpreviousPatchFile=\"$previousPatchesDir/$baseName.patch\"\n\n\t\t\tif ! [ -e \"$activeFile\" ] ; then\n\t\t\t\tlogMessage \"no active file $activeFile - skipping patch\"\n\t\t\t\tcontinue\n\t\t\tfi\n\n\t\t\trm -f \"$currentPatchFile\"\n\n\t\t\t# check for this and other packages in .package list\n\t\t\tpackageList=\"$activeFile.package\"\n\t\t\tthisPackageInList=false\n\t\t\totherPackagesInList=false\n\t\t\tif [ -f \"$packageList\" ]; then\n\t\t\t\tpreviousPackages=$( cat \"$packageList\" )\n\t\t\t\tfor previousPackage in ${previousPackages[@]}; do\n\t\t\t\t\tif [ $packageName == $previousPackage ]; then\n\t\t\t\t\t\tthisPackageInList=true\n\t\t\t\t\telse\n\t\t\t\t\t\totherPackagesInList=true\n\t\t\t\t\tfi\n\t\t\t\tdone\n\t\t\tfi\n\n\t\t\tpatchOk=false\n\t\t\tskipPatch=false\n\n\t\t\tif $thisPackageInList; then\n\t\t\t\tif ! $otherPackagesInList ; then\n\t\t\t\t\t# only this package modified active file\n\t\t\t\t\t# ignore any previous patch and patch .orig file\n\t\t\t\t\tif [ -e \"$activeFile.orig\" ]; then\n\t\t\t\t\t\tcp \"$activeFile.orig\" \"$tempActiveFile\"\n\t\t\t\t\t\tpatchOk=true\n\t\t\t\t\t# use active file if no .orig exists\n\t\t\t\t\telse\n\t\t\t\t\t\tcp \"$activeFile\" \"$tempActiveFile\"\n\t\t\t\t\t\tpatchOk=true\n\t\t\t\t\tfi\n\t\t\t\t# this and others have modified the active file\n\t\t\t\t# attempt to remove the previous patch for this package\n\t\t\t\t# then patch the result\n\t\t\t\telif [ -e \"$previousPatchFile\" ]; then\n\t\t\t\t\tif $patch --reverse -o \"$tempActiveFile\" \"$activeFile\" \"$previousPatchFile\" &> /dev/null ; then\n\t\t\t\t\t\tpatchOk=true\n\t\t\t\t\t# reverse patch failed\n\t\t\t\t\telse\n\t\t\t\t\t\tpatchErrors+=( \"$baseName unable to remove previous patch\" )\n\t\t\t\t\tfi\n\t\t\t\telse\n\t\t\t\t\tpatchErrors+=( \"$baseName no previous patch file\" )\n\t\t\t\tfi\n\t\t\t# this package has not previously modified the active file\n\t\t\t# patch the active file\n\t\t\telse\n\t\t\t\tcp \"$activeFile\" \"$tempActiveFile\"\n\t\t\t\tpatchOk=true\n\t\t\tfi\n\t\t\tpatchSuccess=false\n\t\t\t# a suitable source for the patch was located above\n\t\t\tif $patchOk; then\n\t\t\t\t# attempt to patch the active file with any file ending in .patch\n\t\t\t\t# the first one that successfully creates a forward AND reverse patch is used\n\t\t\t\t# .patchedForInstall provides the patched file for updateActiveFile\n\t\t\t\tpatchFiles=( $( ls \"$patchSourceDir/$baseName\"*.patch ) )\n\t\t\t\tfor patchFile in ${patchFiles[@]};do\n\t\t\t\t\tif $patch --forward -o \"$forwardPatched\" \"$tempActiveFile\" \"$patchFile\" &> /dev/null ; then\n\t\t\t\t\t\t# forward patch succeeded - test reverse patch (both must succeed)\n\t\t\t\t\t\tif $patch --reverse -o /dev/null \"$forwardPatched\" \"$patchFile\" &> /dev/null ; then\n\t\t\t\t\t\t\tpatchSuccess=true\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\tfi\n\t\t\t\t\tfi\n\t\t\t\tdone\n\t\t\t\tif $patchSuccess ; then\n\t\t\t\t\t# save this so forwardPatched (created above) is used for install\n\t\t\t\t\t# when file is installed, currentPatchFile will be copied to previousPatchFile\n\t\t\t\t\t#\tfor a future uninstall or reinstall\n\t\t\t\t\tcp \"$patchFile\" \"$currentPatchFile\"\n\t\t\t\telse\n\t\t\t\t\tpatchErrors+=( \"$baseName patch unsuccesful\" )\n\t\t\t\t\trm -f \"$forwardPatched\"\n\t\t\t\tfi\n\t\t\telif ! $skipPatch ; then\n\t\t\t\tpatchErrors+=( \"$baseName no patch source\" )\n\t\t\tfi\n\t\t\trm -f \"$tempActiveFile\"\n\t\tdone # for activeFile\n\n\t\t# save errors in patchErrors file\n\t\tif ! [ -z \"$patchErrors\" ] ; then\n\t\t\trm -f \"$scriptDir/patchErrors\"\n\t\t\tfor patchError in \"${patchErrors[@]}\"; do\n\t\t\t\tlogMessage \"$patchError\"\n\t\t\t\techo \"$patchError\" >> \"$scriptDir/patchErrors\"\n\t\t\tdone\n\t\t\tsetInstallFailed $EXIT_PATCH_ERROR \"patch error details were saved in $packageName/patchErrors\"\n\n\t\t\t# pre-checks only - direct exit\n\t\t\tif [ $scriptAction == 'CHECK' ]; then\n\t\t\t\texit $EXIT_PATCH_ERROR\n\t\t\t# script will exit since we are still in pre-checks !!\n\t\t\telse\n\t\t\t\tendScript\n\t\t\tfi\n\t\t# no errors - remove the error history\n\t\telse\n\t\t\trm -f \"$scriptDir/patchErrors\"\n\t\tfi\n\tfi # if fileListPatched\nfi # if [ $scriptAction != 'UNINSTALL' ]\n\n# go no further if just checking\nif [ $scriptAction == 'CHECK' ]; then\n\texit $EXIT_SUCCESS\nfi\n\n# in the Victron images, the root FS is read-only and is a minimum size\n# in order to install modificaitons, the root partition needs to be\n#\tremounted read-write and resized to allow mods to be added\n# updateRootToReadWrite calls remount-rw.sh or resize2fs.sh\n#\tthen check to make sure there is sufficient space before allowing installs\n\nupdateRootToReadWrite\n\n# patch files previously used to patch a file are stored in /etc/venus\n# so they track the selected root fs and are erased when Venus OS is updated\nif ! [ -e \"$previousPatchesDir\" ]; then\n\tmkdir -p \"$previousPatchesDir\"\nfi\n# relocate previous patch files\noldPreviousPatchesDir=\"$setupOptionsDir/previousPatches\"\nif [ -e \"$oldPreviousPatchesDir\" ]; then\n\tlogMessage \"relocating previous patches\"\n\tmv \"$oldPreviousPatchesDir\"/* \"$previousPatchesDir\"\n\trm -rf \"$oldPreviousPatchesDir\"\nfi\nunset oldPreviousPatchesDir\n\n# done with pre checks\n#\tprior to this no system mofications have been made\n#\tafter this system modifications may occur\n\nif $installFailed ; then\n\tif ! $userInteraction ; then\n\t\tlogMessage \"ERROR: errors occured during pre-checks - can't continue\"\n\t\t# EXIT HERE if errors occured during pre-checks while running unattended !!!!\n\t\t# the LAST install failure reported to setInstallFailed is used as the exit code\n\t\texit $installExitReason\t# EXIT HERE !!!!\n\t# command line install request failed - reset scriptAction to NONE\n\t# so prompts will be shown - eg\n\t#\tto show the log or\n\t#\tto trigger a manual uninstall\n\telif  [ $scriptAction == 'INSTALL' ]; then\n\t\tlogMessage \"ERROR: install failed during pre-checks - select another action\"\n\t\tscriptAction='NONE'\n\tfi\nfi\n\n# create installed files (and services) directory\nif [ ! -d \"$installedFilesDir\" ]; then\n\tmkdir \"$installedFilesDir\"\nfi\n\ninstallPreChecks=false\n\n\n#### do standard prompting, automatic install/uninstall then exit\nif [ \"$standardPromptAndActions\" == 'yes' ]; then\n\t# prompt only if action hasn't been set yet (from command line)\n\tif [ \"$scriptAction\" == 'NONE' ]; then\n\t\tstandardActionPrompt\n\tfi\n\tendScript 'INSTALL_FILES' 'INSTALL_SERVICES' 'ADD_DBUS_SETTINGS'\nfi\n\n# otherwise continue with the setup script \n\n#### continue executing the setup script which sourced this file\n"
  },
  {
    "path": "HelperResources/DbusSettingsResources",
    "content": "#!/bin/bash\n\n# DbusSettingsResources for SetupHelper\n#\n# contains a functions and variables necessary to access dbus Settings parameters\n# it should be sourced by scripts setting, creating and removing dbus settings\n#\n# dbus Settings is not operational during system boot when some setup scripts may\n# need to make settings changes\n# These functions check to see if the settings system is operational and defer\n# the set/create/remove activity so the calling script may continue\n\n# dbus Settings funcitons\n# These functions encapsulate an interface to dbus Settings\n# NOTE: dbus Settings resources are not always active when it is necessary for\n# scripts to make changes or create/remove settings\n# it is up to the caller to insure dbus Settings resources are active before callling\n# these functions\n# a dbus exeption error will be logged if settings are not active yet\n\n\n# updateDbusStringSetting\n# updateDbusIntSetting\n# updateDbusRealSetting\n#   updates a dbus setting parameter with a new value\n#\n# if the setting does not exist, it is created\n# but max and min values are not set and the default is \"\", 0 or 0.0 depending on data type\n# if these are needed use the dbus command directly\n#   this can also be faster if lots of settings must be created at the same time\n#\n# other data types may exist and would need their own function\n#\n# $1 is the path to the setting starting with /Settings\n# $2 is the new value\n#\n# if the setting does not yet exist, it is created, then updated to the new value\n\nupdateDbusStringSetting ()\n{\n\t# don't do any work if install has already failed\n\tif $installFailed; then\n\t\treturn\n\tfi\n\n    dbus-send --system --print-reply=literal --dest=com.victronenergy.settings \"$1\"\\\n            com.victronenergy.BusItem.GetValue &> /dev/null\n    if (( $? != 0 )); then\n        logMessage \"creating dbus Setting $1\"\n        dbus -y com.victronenergy.settings / AddSettings \"%[ {\\\"path\\\":\\\"$1\\\", \\\"default\\\":\\\"\\\"} ]\" &> /dev/null\n    fi\n\n\tdbus -y com.victronenergy.settings \"$1\" SetValue -- \"$2\" &> /dev/null\n}\n\n\nupdateDbusIntSetting ()\n{\n\t# don't do any work if install has already failed\n\tif $installFailed; then\n\t\treturn\n\tfi\n\n    dbus-send --system --print-reply=literal --dest=com.victronenergy.settings \"$1\"\\\n            com.victronenergy.BusItem.GetValue &> /dev/null\n    if (( $? != 0 )); then\n        logMessage \"creating dbus Setting $1\"\n        dbus -y com.victronenergy.settings / AddSettings \"%[ {\\\"path\\\":\\\"$1\\\", \\\"default\\\":0} ]\" &> /dev/null\n    fi\n\n    dbus -y com.victronenergy.settings \"$1\" SetValue -- \"$2\" &> /dev/null\n}\n\n\nupdateDbusRealSetting ()\n{\n\t# don't do any work if install has already failed\n\tif $installFailed; then\n\t\treturn\n\tfi\n\n    dbus-send --system --print-reply=literal --dest=com.victronenergy.settings \"$1\"\\\n            com.victronenergy.BusItem.GetValue &> /dev/null\n    if (( $? != 0 )); then\n        logMessage \"creating dbus Setting $1\"\n        dbus -y com.victronenergy.settings / AddSettings \"%[ {\\\"path\\\":\\\"$1\\\", \\\"default\\\":0.0} ]\" &> /dev/null\n    fi\n\n    dbus -y com.victronenergy.settings \"$1\" SetValue -- \"$2\" &> /dev/null\n}\n\n\n\n# addAllDbusSettings adds settings from DbusSettingsList in the package directory\n# the format of each line is:\n# {\"path\":\"/Settings/GuiMods/ShortenTankNames\", \"default\":1, \"min\":0, \"max\":1}\n# min and max are optional\n\naddAllDbusSettings ()\n{\n\tlocal settings\n\n\tif [ -f \"$scriptDir/DbusSettingsList\" ]; then\n\t\tlogMessage \"updating dbus Settings\"\n\t\twhile read -r line || [[ -n \"$line\" ]]; do\n\t\t\tsettings+=\"$line, \"\n\t\tdone < \"$scriptDir/DbusSettingsList\"\n\n\t\tdbus -y com.victronenergy.settings / AddSettings \"%[ $settings  ]\" &> /dev/null\n\tfi\n}\n\n# same as above but removes them\n# typically settings are retained when removing a package so\n# the developer must make this call specifically in the setup script's UNINSTALL section\n# if they wish to remove the settings\n\nremoveAllDbusSettings ()\n{\n\tlocal settings\n\n\tif [ -f \"$scriptDir/DbusSettingsList\" ]; then\n\t\tlogMessage \"removing dbus Settings\"\n\t\twhile read -r line || [[ -n \"$line\" ]]; do\n\t\t\t\tsettings+=$( echo $line | awk -F[:,] '{print $2, \",\"}' )\n\t\tdone < \"$scriptDir/DbusSettingsList\"\n\n\t\tdbus -y com.victronenergy.settings / RemoveSettings \"%[ $settings  ]\"\n\tfi\n}\n\n\n\n# removeDbusSettings removes the setting from dbus Settings\n#\n# all parameters are each a quoted path to the setting to be removed\n# e.g., removeDbusSettings \"/Settings/foo\" \"/Settings/bar\"\n# (including all settings in one dbus call is much faster)\n\nremoveDbusSettings ()\n{\n    logMessage \"removing dbus Settings $@\"\n    local settings=$(echo \"$@\" | sed -e s_^_\\\"_ -e s_\\$_\\\"_ -e s_\\ _'\", \"'_g)\n    dbus -y com.victronenergy.settings / RemoveSettings \"%[ $settings ]\" &> /dev/null\n}\n\n\n# setSetting updates the dbus setting parameter\n# the setting must already exist or the update will fail\n# (the setting can not be created without knowing the data type(s))\n#\n# $1 is the new value\n# $2 is the setting path\n\nsetSetting ()\n{\n\t# don't do any work if install has already failed\n\tif $installFailed; then\n\t\treturn\n\tfi\n\n    dbus -y com.victronenergy.settings $2 SetValue $1 &> /dev/null\n}\n\n# move a setting from setup options or from previous dbus Setting\n# $1 is the setup options path\n# $2 is the old dbus path (has priority over setup option)\n# $3 is the new dbus path\n# dbus paths start with /Settings\n# if specified, the setup option file must include a value\n#   that value has priority over the old dbus parameter\n#\n# setup options can either contain a value or be a flag file\n# for flag files, the file will be empty and the state of the option\n# depends on the presence of the file (true) or absense of the file (false)\n#\n# Note: this function does NOT create or remove any old option or Setting\n# use other functions or commands to do so\n\nmoveSetting ()\n{\n\t# don't do any work if install has already failed\n\tif $installFailed; then\n\t\treturn\n\tfi\n\n    local setupOption=\"$1\"\n    local oldDbusPath=$2\n    local newDbusPath=$3\n\n    if [ ! -z \"$oldDbusPath\" ]; then\n        oldSetting=$(dbus-send --system --print-reply=literal --dest=com.victronenergy.settings\\\n            $oldDbusPath com.victronenergy.BusItem.GetValue 2> /dev/null | awk '{print $3}')\n    elif [ ! -z $setupOption ]; then\n        if [ -f \"$setupOption\" ]; then\n            oldSetting=$(cat \"$setupOption\")\n            # flag file - old setting is true (1)\n            if [ -z $oldSetting ]; then\n                oldSetting=1\n            fi\n        # file did not exist - assume a false value for a flag file\n        else\n            oldSetting=0\n        fi\n    else\n        oldSetting=\"\"\n    fi\n    if [ ! -z $oldSetting ] && [ ! -z \"$newDbusPath\" ]; then\n        dbus -y com.victronenergy.settings $newDbusPath SetValue $oldSetting &> /dev/null\n    fi\n}\n"
  },
  {
    "path": "HelperResources/EssentialResources",
    "content": "#!/bin/bash\n\n# EssentialResources for SetupHelper\n# contains a variables necessary for all setup helper scripts\n#\n# sourced from IncludeHelpers, packageManagerEnd.sh and reinstallMods\n\n# get the full, unambiguous path to this script (and therefore to script that sourced it)\nscriptDir=\"$( cd \"$(dirname $0)\" >/dev/null 2>&1 ; /bin/pwd -P )\"\npackageRoot=\"$( dirname $scriptDir )\"\npackageName=$( basename \"$scriptDir\" )\n\nshortScriptName=$(basename \"$scriptDir\")/$(basename \"$0\")\nfullScriptName=\"$scriptDir/$(basename \"$0\")\"\n\nif [ -f \"/opt/victronenergy/version\" ]; then\n\tvenusVersion=\"$(cat /opt/victronenergy/version | head -n 1)\"\nelse\n\tvenusVersion=\"\"\nfi\n\ninstalledVersionPrefix=\"/etc/venus/installedVersion-\"\ninstalledVersionFile=\"$installedVersionPrefix\"$packageName\n\ninstalledFilesDir=\"/etc/venus/installedModifications\"\ninstalledFilesList=\"$installedFilesDir/installedFiles\"-$packageName\ninstalledServicesList=\"$installedFilesDir/installedServices\"-$packageName\n\n# set up pointers to package files\n#\tbased on the actual package for compatibility with older packages\npkgFileSets=\"$scriptDir/FileSets\"\nfileSet=\"$pkgFileSets/$venusVersion\"\nversionIndependentFileSet=\"$pkgFileSets/VersionIndependent\"\n# location of patch files\npatchSourceDir=\"$pkgFileSets/PatchSource\"\naltOrigFileDir=\"$pkgFileSets/AlternateOriginals\"\n\nservicesDir=\"$scriptDir/services\"\n\n\n# LogHandler functions and variables\n\n# enable logging to console\n# scripts can disable logging by setting \n# logToConsole to false AFTER sourcing EssentialResources\nlogToConsole=true\n\nrunFromPm=false\n\n# write a message to log file and console\n\nlogMessage ()\n{\n\t# send to stdout if running from PackageManager\n\t#\tit will include the message in its log file\n\tif $runFromPm ; then\n\t\techo \"$shortScriptName: $*\"\n\t# to console\n    elif $logToConsole ; then\n        echo \"$*\"\n    # to PackageManager log file if no other choices\n    #\tNOTE: PackageManager's logger service will manage the log file size\n    #\t\thowever if it is not running /data could eventually fill up\n\telse\n\t\techo \"$shortScriptName: $*\" | tai64n >> $logFile\n    fi\n}\n\n# create log file and directory tree if it does not exist yet\nlogDir=\"/var/log/PackageManager\"\nlogFile=\"$logDir/current\"\nif ! [ -e \"$logDir\" ]; then\n\tmkdir -p \"$logDir\"\n\ttouch \"$logFile\"\n\tlogMessage \"creating log file and directory\"\nfi\n\n# remove old log file if it still exists\noldLogFile=\"/var/log/SetupHelper\"\nif [ -e \"$oldLogFile\" ]; then\n\trm -rf \"$oldLogFile\"\nfi\n\n\n# rc local file that calls reinstallMods\n#\trcS.local avoids conflicts with mods that blindly replace /data/rc.local\nrcLocal=\"/data/rcS.local\"\n\n# defined exit codes - must be consistent between all setup scripts and reinstallMods\n# and PackageManager.py\nEXIT_SUCCESS=0\nEXIT_REBOOT=123\nEXIT_RESTART_GUI=124\nEXIT_ERROR=255\t# unknown error\nEXIT_INCOMPATIBLE_VERSION=254\nEXIT_INCOMPATIBLE_PLATFORM=253\nEXIT_FILE_SET_ERROR=252\nEXIT_OPTIONS_NOT_SET=251\nEXIT_RUN_AGAIN=250\nEXIT_ROOT_FULL=249\nEXIT_DATA_FULL=248\nEXIT_NO_GUI_V1=247\nEXIT_PACKAGE_CONFLICT=246\nEXIT_PATCH_ERROR=245\n\n\n# directory that holds script's options\n# options were removed from the script directory so they are preserved when the package is reinstalled\nsetupOptionsRoot=\"/data/setupOptions\"\nsetupOptionsDir=\"$setupOptionsRoot\"/$packageName\n\n# packages managed by SetupHelper\npackageListFile=\"/data/packageList\"\n\nqmlDir=/opt/victronenergy/gui/qml\n\n\n# setInstallFailed sets flags to prevent further install steps\n#\tand insure the package is uninstalled completely\n#\n#\t$1 indicates the reason for the failure and will evenutally be uused\n#\treport the failure reason when exiting the script\n#\n#\tany remaining paremeters are passed to logMessage\n#\t\tand also saved in installFailMessage for others to use\n#\tthe message is also sent to stderr if not running from the command line\n#\t\tthis allows PackageManager to report the full reason for failure\n#\n#\ta setup script can be run from the console, or from another script or program (unattended)\n#\twhen running from the console\n#\t\tsetInstallFailed will report errors and return to the caller\n#\n#\tif running unattended and if the script action is INSTALL\n#\t\tduring the precheck period (before any system modification)\n#\t\t\tsetInstallFailed will EXIT WITHOUT RETURNING TO THE CALLER !!!!!\n#\t\tafter the precheck period, system modifications may have been made so\n#\t\t\tthe scriptAction is changed to UNINSTALL so the modifictions can be reversed\n#\totherwise, setInstallFailed just logs the error\n#\n# installFailed is set here so that additional install operations will not be performed\n\ninstallFailed=false\ninstallExitReason=$EXIT_ERROR\nuninstallExitReason=$EXIT_ERROR\ninstallFailMessage=\"\"\ninstallPreChecks=true\ninstallFailCount=0\nuninstallFailed=false\n\nsetInstallFailed ()\n{\n\tlocal reason\n\n\t(( installFailCount += 1 ))\n\tif [ ! -z \"$1\" ]; then\n\t\treason=$1\n\t# no reson specified - use the generaic error exit code\n\telse\n\t\treason=EXIT_ERROR\n\tfi\n\tinstallFailMessage=\"${@:2}\"\n\tif [ ! -z \"$installFailMessage\" ]; then\n\t\t# output error to stderr if run from PackageManager\n\t\tif $runFromPm ; then\n\t\t\techo \"$installFailMessage\" >&2\n\t\t# otherwise output to log (and/or stdout/console)\n\t\telse\n\t\t\tlogMessage \"ERROR: $installFailMessage\"\n\t\tfi\n\tfi\n\n\tif [ $scriptAction == 'UNINSTALL' ]; then\n\t\tuninstallExitReason=$reason\n\t\tuninstallFailed=true\n\telse\n\t\tinstallExitReason=$reason\n\t\tinstallFailed=true\n\tfi\n\tif [ $scriptAction == 'INSTALL' ]; then\n\t\t# after \"pre-checks\" system mofifications may already have occured\n\t\t#\tso an uninstall needs to follow the install\n\t\tif ! $installPreChecks ; then\n\t\t\tscriptAction='UNINSTALL'\n\n\t\t# during \"pre-checks\" failures occur before any system mofifications\n\t\t#\tif no user interaction EXIT NOW - DO NOT RETURN TO THE CALLER !!!!!\n\t\telif ! $userInteraction ; then\n\t\t\texit $installExitReason\n\t\tfi\n\tfi\n}\n\n# set global machine type\nif [ -f /etc/venus/machine ]; then\n\tmachine=$(cat /etc/venus/machine)\nelse\n\tmachine=\"\"\n\tsetInstallFailed $EXIT_INCOMPATIBLE_PLATFORM \"can't determine Venus device type\"\nfi\n\n# make sure rootfs is mounted R/W & and resized to allow space for replacement files\n#\tarbitrary minimum size of 3 MB\n# this needs to be called before root fs mods are made.\n#\tCommonResources calls this but if you source a subset of helper resources\n#\tthat script needs to find a place to call updateRootToReadWrite\n\nupdateRootToReadWrite ()\n{\n\tif ! $installFailed; then\n\t\trootMinimumSize=3\n\t\tavailableSpace=$(df -m / | tail -1 | awk '{print $4}')\n\n\t\t# remount read-write\n\t\tif (( $(mount | grep ' / ' | grep -c 'rw') == 0 )); then\n\t\t\t# only remount read-write for CCGX\n\t\t\tif [ \"$machine\" == \"ccgx\" ]; then\n\t\t\t\tif [ -f /opt/victronenergy/swupdate-scripts/remount-rw.sh ]; then\n\t\t\t\t\tlogMessage \"remounting root read-write\"\n\t\t\t\t\t/opt/victronenergy/swupdate-scripts/remount-rw.sh\n\t\t\t\tfi\n\t\t\t# remount and resize for other platforms\n\t\t\telif [ -f /opt/victronenergy/swupdate-scripts/resize2fs.sh ]; then\n\t\t\t\t/opt/victronenergy/swupdate-scripts/resize2fs.sh\n\t\t\t\tavailableSpace=$(df -m / | tail -1 | awk '{print $4}')\n\t\t\t\tlogMessage \"remounting root read-write and resizing - $availableSpace MB now available\"\n\t\t\tfi\n\t\t\t# check to see if remount was successful\n\t\t\tif (( $(mount | grep ' / ' | grep -c 'rw') == 0 )); then\n\t\t\t\tsetInstallFailed $EXIT_ROOT_FULL \"ERROR: unable to remount root read-write - can't continue\"\n\t\t\tfi\n\t\t# root already read-write, attempt to resize if space is limited (CCGX can't resize)\n\t\telif (( $availableSpace < $rootMinimumSize )) && [ \"$machine\" != \"ccgx\" ]; then\n\t\t\tif [ -f /opt/victronenergy/swupdate-scripts/resize2fs.sh ]; then\n\t\t\t\t/opt/victronenergy/swupdate-scripts/resize2fs.sh\n\t\t\t\tavailableSpace=$(df -m / | tail -1 | awk '{print $4}')\n\t\t\t\tlogMessage \"resized root - $availableSpace MB now available\"\n\t\t\tfi\n\t\tfi\n\tfi\n\tif ! $installFailed; then\n\t\t# make sure the root partition has space for the package\n\t\tif (( $availableSpace < $rootMinimumSize )); then\n\t\t\tsetInstallFailed $EXIT_ROOT_FULL \"no room for modified files on root ($availableSpace MB remaining) - can't continue\"\n\t\tfi\n\tfi\n}\n\n\n# convert a version string to an integer to make comparisions easier\n# the Victron format for version numbers is: vX.Y~Z-large-W\n# the ~Z portion indicates a pre-release version so a version without it is \"newer\" than a version with it\n# the -W portion has been abandoned but was like the ~Z for large builds and is IGNORED !!!!\n#\tlarge builds now have the same version number as the \"normal\" build\n#\n# the version string passed to this function allows for quite a bit of flexibility\n#\tany alpha characters are permitted prior to the first digit\n#\tup to 3 version parts PLUS a prerelease part are permitted\n#\t\teach with up to 4 digits each -- MORE THAN 4 digits is indeterminate\n#\tthat is: v0.0.0d0  up to v9999.9999.9999b9999 and then v9999.9999.9999 as the highest priority\n#\tany non-numeric character can be used to separate main versions\n#\tspecial significance is assigned to single caracter separators between the numeric strings\n#\t\tb or ~ indicates a beta release\n#\t\ta indicates an alpha release\n#\t\td indicates an development release\n# \t\tthese offset the pre-release number so that b/~ has higher numeric value than any a\n#\t\t\tand a has higher value than d separator\n#\n# a blank version or one without at least one number part is considered invalid\n# alpha and beta seperators require at least two number parts\n#\n# returns 0 if conversion succeeeded, 1 if not\n# the value integer is returned in $versionNumber\n# a status text string is returned in $versionStringToNumberStatus\n#\tand will include the string passed to the function\n#\tas well as the converted number if successful and the type of release detected\n#\t\tor an error reason if not\n#\n\n\nfunction versionStringToNumber ()\n{\n\tlocal version=\"$*\"\n\tlocal numberParts\n\tlocal versionParts\n\tlocal numberParts\n\tlocal otherParts\n\tlocal other\n\tlocal number=0\n\tlocal type='release'\n\n\t# split incoming string into\n\t# an array of numbers: major, minor, prerelease, etc\n\t# and an array of other substrings\n\t# the other array is searched for releasy type strings and the related offest added to the version number\n\t\n\tread -a numberParts <<< $(echo $version | tr -cs '0-9' ' ')\n\tnumberPartsLength=${#numberParts[@]}\n\tif (( $numberPartsLength == 0 )); then\n\t\tversionNumber=0\n\t\tversionStringToNumberStatus=\"$version: invalid, missing major version\"\n\t\treturn 1\n\tfi\n\tif (( $numberPartsLength >= 2 )); then\n\t\tread -a otherParts <<< $(echo $version | tr -s '0-9' ' ')\n\t\n\t\tfor other in ${otherParts[@]}; do\n\t\t\tcase $other in\n\t\t\t\t'b' | '~')\n\t\t\t\t\ttype='beta'\n\t\t\t\t\t(( number += 60000 ))\n\t\t\t\t\tbreak ;;\n\t\t\t\t'a')\n\t\t\t\t\ttype='alpha'\n\t\t\t\t\t(( number += 30000 ))\n\t\t\t\t\tbreak ;;\n\t\t\t\t'd')\n\t\t\t\t\ttype='develop'\n\t\t\t\t\tbreak ;;\n\t\t\tesac\n\t\tdone\n\tfi\n\n\t# if release all parts contribute to the main version number\n\t#\tand offset is greater than all prerelease versions\n\tif [ \"$type\" == \"release\" ] ; then\n\t\t(( number += 90000 ))\n\t# if pre-release, last part will be the pre release part\n\t#\tand others part will be part the main version number\n\telse\n\t\t(( numberPartsLength-- ))\n\t\t(( number += 10#${numberParts[$numberPartsLength]} ))\n\tfi\n\t# include core version number\n\t(( number += 10#${numberParts[0]} * 10000000000000 ))\n\tif (( numberPartsLength >= 2)); then\n\t\t(( number += 10#${numberParts[1]} * 1000000000 ))\n\tfi\n\tif (( numberPartsLength >= 3)); then\n\t\t(( number += 10#${numberParts[2]} * 100000 ))\n\tfi\n\n\tversionNumber=$number\n\tversionStringToNumberStatus=\"$version:$number $type\"\n\treturn 0\n}\n\n\n\n# compares two version strings\n#\n# missing verions are treated as 0\n#\n# returns 0 if they are equal\n# returns 1 if the first is newer than the second\n# returns -1 if the second is newer than the first\n\nfunction compareVersions ()\n{\n\tlocal versionNumber2\n\n\tif [ -z $2 ]; then\n\t\tversionNumber2=0\n\telse\n\t\tversionStringToNumber $2\n\t\tversionNumber2=$versionNumber\n\tfi\n\tif [ -z $1 ]; then\n\t\tversionNumber=0\n\telse\n\t\tversionStringToNumber $1\n\tfi\n\n\tif (( versionNumber == versionNumber2 ));then\n\t\treturn 0\n\telif (( versionNumber > versionNumber2 ));then\n\t\treturn 1\n\telse\n\t\treturn -1\n\tfi\n}\n"
  },
  {
    "path": "HelperResources/IncludeHelpers",
    "content": "#!/bin/sh\n\n# this script sources helper Resources into the setup script\n#\n# for backward compatibility, CommonResources in the SetupHelper directory\n#\tlinks to this file, not CommonResources\n#\tCommonResources previously sourced the other files\n#\tnow sourcing all resource files is done from here\n#\n# only the helper files located is the SetupHelper directory are used\n#\tprevious versions chose between this and a helper file set in the package directory\n#\tbut changes in SetupHelper to use a local copy of patch made this not possible\n#\n# this script should be sourced in the setup script before any other activities\n\npkgDir=\"$( cd \"$(dirname $0)\" >/dev/null 2>&1 ; /bin/pwd -P )\"\npkgRoot=\"$( dirname \"$pkgDir\")\"\npkgName=$( basename $pkgDir )\nhelperResourcesDir=\"$pkgRoot/SetupHelper/HelperResources\"\nlogDir=\"/var/log/PackageManager\"\nlogFile=\"$logDir/current\"\n\nif ! [ -e \"$helperResourcesDir\" ]; then\n\techo \"$pkgName: helper files not found - can't continue\" | tee -a \"/data/log/SetupHelper\"\n\texit 1\nfi\n\n# if we get here, helper files were located - source the files\nhelperFileList=( EssentialResources ServiceResources DbusSettingsResources )\nfor file in ${helperFileList[@]}; do\n\tif [ -f \"$helperResourcesDir/$file\" ]; then\n\t\tsource \"$helperResourcesDir/$file\"\n\telse\n\t\techo \"$pkgName: helper file $file not found - can't continue\" | tee -a \"$logFile\"\n\t\texit 1\n\tfi\ndone\n\n# now transfer control to CommonResoures - it may not return !\nif [ -f \"$helperResourcesDir/CommonResources\" ]; then\n\tsource \"$helperResourcesDir/CommonResources\"\nelse\n\techo \"$pkgName: helper file CommonResources not found - can't continue\" | tee -a \"$logFile\"\n\texit 1\nfi\n\n\n"
  },
  {
    "path": "HelperResources/LogHandler",
    "content": "# dummy file to prevent failure with older package setup scripts\n\n# logging is now in EssentialResources\n"
  },
  {
    "path": "HelperResources/ServiceResources",
    "content": "#!/bin/bash\n\n#!/bin/bash\n# ServiceManager for SetupHelper\n# contains a functions to install and remove a package's service\n#\n# If an active copy of the service already exists, the run and log/run files are updated\n# ONLY if there are changes, then the service and/or the logger will be restarted.\n# This leaves other files managed by supervise untouched.\n#\n# in Venus OS starting with v2.90~3, /service is mounted as a tmpfs (RAM disk)\n# /opt/victronenergy/service is copied to /service ONLY at boot time\n# so new services need to be copied to /opt/victronenergy/service for boot processing\n# AND to /service so they run immediately\n#\n# svc -u /service/<service name> starts a service that is not already running\n# svc -d /service/<service name> stops a service and will not restart\n# these are \"temporary\" and don't survive a system boot\n# svc -t /service/<service name> sends the service a TERM command\n#\tif the service was up at the time, it restarts\n#\tif the service was down at the time, it is NOT started\n#\n# the /service/<service name>/down flag file controls the state of a service at boot time:\n#\tif the file exists, the service won't start automatically at boot or when created\n#\tif the file does not exist, the service will start at boot or immediately when it is created\n#\n# if the services manager (svscan) is not up, or the real service directory does not yet exist\n#\tsome steps will be skipped to avoid errors in calling daemontools functions\n#\n# more info here:\n#\thttps://cr.yp.to/daemontools/svc.html\n#\thttps://cr.yp.to/daemontools/supervise.html\n#\thttps://cr.yp.to/daemontools/svstat.html\n#\thttps://cr.yp.to/daemontools/svok.html\n\n# storage for services\n#\t/service is the location where services run from\n#\tand is mounted as a temp FS\n#\n# contents of serviceDir is copied to /service during boot\n#\tbut at no other time, so when services are added\n#\tthey need to be copied to both locations\nserviceDir=\"/opt/victronenergy/service\"\n\n\n# check to see if services manager is running\nsvscanIsUp ()\n{\n\tpgrep -lx svscan &> /dev/null\n\tif (( $? == 0 )) ; then\n\t\treturn 0\n\telse\n\t\treturn 1\n\tfi\n}\n\n# check to see if named service is up\nserviceIsUp ()\n{\n\tif ! svscanIsUp ; then\n\t\treturn 1\n\telif [ ! -e \"/service/$1\" ]; then\n\t\treturn 1\n\telif [ $(svstat \"/service/$1\" | awk '{print $2}') == \"up\" ]; then\n\t\treturn 0\n\telse\n\t\treturn 1\n\tfi\n}\n\n\n#\n# removeService cleanly removes the service\n#\n# When uninstalling from within PackageManager,\n#\tthe PackageManager service is only removed from servicesDir and from installedServicesList\n#\tthe copy in /services is marked so it will not run again after PackageManager exits\n\n\nremoveService ()\n{\n    # no service specified\n    if (( $# < 1 )); then\n        return\n    fi\n\tlocal serviceName=\"$1\"\n\t\n\tlocal shutdownService=true\n\tif [ \"$serviceName\" == \"PackageManager\" ] && $runFromPm ; then\n\t\tshutdownService=false\n\tfi\n\n    if [ -e \"$serviceDir/$serviceName\" ]; then\n        logMessage \"removing $serviceName service\"\n\t\t# set flag so PackageManager won't restart after a reboot\n\t\tif ! $shutdownService ; then\n\t\t\tlogMessage \"$serviceName service will remain running but will not run again\"\n\t\t\ttouch \"/service/$serviceName/down\"\n\t\t\t# if PackageManager is running, mark it so it will not restart when it exits\n\t\t\tif serviceIsUp $serviceName ; then\n\t\t\t\tsvc -o \"/service/$serviceName\"\n\t\t\tfi\n\t\t# stop the service if it is currently running\n\t\telif serviceIsUp $serviceName ; then\n\t\t\tsvc -d \"/service/$serviceName\"\n\t\tfi\n\t\tif $shutdownService && serviceIsUp \"$serviceName/log\" ; then\n\t\t\tsvc -d \"/service/$serviceName/log\"\n\t\tfi\n\n\t\t# supervise processes may hang around after removing the service so save info and kill them after removal\n\t\tpids=\"\"\n\t\twhile read -u 9 line; do\n\t\t\tread s uid pid ppid vsz rss tty stime time cmd blah <<< \"$line\"\n\t\t\tif [ $cmd == 'supervise' ]; then\n\t\t\t\tpids+=\"$pid \"\n\t\t\telif [ $cmd == 'multilog' ]; then\n\t\t\t\tpids+=\"$ppid \"\n\t\t\tfi\n\t\tdone 9<<< $(ps -lw | grep $serviceName)\n\n\t\t# remove the service directory\n\t\trm -rf \"$serviceDir/$serviceName\"\n\n\t\t# /service is mounted as tmpfs so the service needs to be removed from /service also\n\t\t# when uninstalling SetupHelper from withing PackageManager, do NOT delete the running service !\n\t\t#\t(after the next reboot, /service/PackageManager won't becopied from serviceDir)\n\t\t#\t(logger remains running until the next boot)\n\t\tif $shutdownService ; then\n\t\t\trm -rf \"/service/$serviceName\"\n\n\t\t\t# also kill the supervise processes\n\t\t\tkill $pids\n\t\tfi\n    fi\n\n\t# remove service from installed services list\n\tif [ -f \"$installedServicesList\" ]; then\n\t\tgrep -v \"$serviceName\" \"$installedServicesList\" | tee \"$installedServicesList\" > /dev/null\n\tfi\n}\n\n\n# installService adds the service to the /service directory or updates an existing one\n#\n# If the service does not yet exist, it is created\n# If the service already exists, installService will\n# update the service files then restart the service and the logger\n\n\n# The service usually starts automatically within a few seconds of creation.\n# installService waits 10 seconds to see if the service starts on its own\n#\tif not, it will be started\n#\n# The service may contain a \"down\" flag file. If present, the service won't be started.\n# This allows the service to be started manually later.\n# If the down flag is present the service will not start at boot.\n#\n#\n# $1 is the service name -- that is the name of the service in /service\n# \tthe package name will be used as the service name if not specified on the command line\n#\n# $2 is the directory in the script directory to be copied to the service in /service\n#\t(this includes the run and control (down) files)\n# \tthe default is 'service' in the package directory\n#\n# for most packages with one service, the defaults are fine\n# however if a package needs to install more than one service\n#\tthen the service name and directory must be specified\n#\tinstallService \"PackageManager\" \"servicePM\"\n#\tinstallService \"SetupHelper\" \"serviceSH\"\n#\tservicePM/run would include a call to /data/SetupHelper/PackageManager.py\n#\tserviceSH/run would include a call to /data/SetupHelper/SetupHelper.sh\n\ninstallService ()\n{\n\t# don't do any work if install has already failed\n\tif $installFailed; then\n\t\treturn\n\tfi\n\n\tlocal serviceName=\"\"\n    if (( $# >= 1 )); then\n        serviceName=$1\n\telse\n\t\tserviceName=$packageName\n    fi\n\n    local servicePath=\"\"\n    if (( $# >= 2 )); then\n        servicePath=\"$scriptDir/$2\"\n\telif [ -e \"$servicesDir/$serviceName\" ]; then\n\t\tservicePath=\"$servicesDir/$serviceName\"\n\telif [ -e \"$scriptDir/service\" ]; then\n\t\tservicePath=\"$scriptDir/service\"\n    fi\n\t\n    # no service to install\n    if [ ! -e \"$servicePath\" ]; then\n\t\tsetInstallFailed $EXIT_ERROR \"service $service not found - can't continue\"\n\t\treturn\n    fi\n\n    if [ -L \"$serviceDir/$serviceName\" ]; then\n        logMessage \"removing old $serviceName service (was symbolic link)\"\n\t\tremoveService $serviceName\n    fi\n\n\t# add service to the installed services list (used for uninstallAll)\n\t# do this before actually modifying things just in case there's an error\n\t#\tthat way the uninstall is assured\n\techo \"$serviceName\" >> \"$installedServicesList\"\n\n    # service not yet installed, COPY service's directory (run files) to the service directory(s)\n\trm -rf \"$serviceDir/$serviceName\"\n\tcp -R \"$servicePath\" \"$serviceDir/$serviceName\"\n\tlocal loggerOk=false\n\tif [ -e \"$serviceDir/$serviceName/log\" ] && ! [ -e \"$serviceDir/$serviceName/log/down\" ]; then\n\t\tloggerOk=true\n\telse\n\t\tlogMessage \"$serviceName logger disabled (down flag) or does not exist\"\n\t\tloggerOk=false\n\tfi\n\n    if [ ! -e \"/service/$serviceName\" ]; then\n        logMessage \"installing $serviceName service\"\n\t\tcp -R \"$servicePath\" \"/service/$serviceName\"\n\n\t\t# if down flag is NOT set, check every second for service to start automatically\n\t\t# then start it here if it is not running after 10 seconds\n\t\tif [ -f \"$serviceDir/$serviceName/down\" ]; then\n\t\t\tlogMessage \"$serviceName not (re)started - must be started manually (down flag set)\"\n\t\telif ! svscanIsUp ; then\n\t\t\tlogMessage \"services manager (svscan) not yet up - $serviceName should start automatically later\"\n\t\telse\n\t\t\tlocal delayCount=10\n\t\t\tlocal serviceRunning=false\n\t\t\tlocal loggerRunning=false\n\t\t\twhile (( $delayCount > 0 )); do\n\t\t\t\tif serviceIsUp $serviceName ; then\n\t\t\t\t\tserviceRunning=true\n\t\t\t\tfi\n\t\t\t\tif serviceIsUp $serviceName/log ; then\n\t\t\t\t\tloggerRunning=true\n\t\t\t\tfi\n\t\t\t\tif $serviceRunning && ( $loggerRunning || ! $loggerOk ); then\n\t\t\t\t\tbreak\n\t\t\t\tfi\n\t\t\t\t# only report wait once\n\t\t\t\tif (( delayCount == 10 )); then\n\t\t\t\t\techo \"waiting for $serviceName service to start\"\n\t\t\t\tfi\n\t\t\t\tsleep 1\n\t\t\t\t(( delayCount-- ))\n\t\t\tdone\n\t\t\tif $loggerOk ; then\n\t\t\t\tif $loggerRunning ; then\n\t\t\t\t\tlogMessage \"$serviceName logger running\"\n\t\t\t\telse\n\t\t\t\t\tlogMessage \"starting $serviceName logger\"\n\t\t\t\t\tsvc -u \"/service/$serviceName/log\"\n\t\t\t\tfi\n\t\t\tfi\n\t\t\tif $serviceRunning; then\n\t\t\t\tlogMessage \"$serviceName service is running\"\n\t\t\telse\n\t\t\t\tlogMessage \"starting $serviceName service\"\n\t\t\t\tsvc -u \"/service/$serviceName\"\n\t\t\tfi\n\t\tfi\n\n    # service already installed - only copy changed files, then restart service if it is running\n    else\n        if [ -f \"/service/$serviceName/log/run\" ]; then\n            cmp -s \"$servicePath/log/run\" \"/service/$serviceName/log/run\" > /dev/null\n            if (( $? != 0 )); then\n\t\t\t\tlogMessage \"updating $serviceName logger run file\"\n\t\t\t\tcp \"$servicePath/log/run\" \"/service/$serviceName/log/run\"\n            fi\n\t\t\tif [ -e \"$servicePath/log/down\" ]; then\n\t\t\t\tsvc -d \"/service/$serviceName/log\"\n\t\t\telse\n\t\t\t\trm -f \"/service/$serviceName/log/down\"\n\t\t\t\tif serviceIsUp \"$serviceName/log\" ; then\n\t\t\t\t\techo \"restarting $serviceName logger\"\n\t\t\t\t\tsvc -t \"/service/$serviceName/log\"\n\t\t\t\telse\n\t\t\t\t\tlogMessage \"starting $serviceName logger\"\n\t\t\t\t\tsvc -u \"/service/$serviceName/log\"\n\t\t\t\tfi\n\t\t\tfi\n        fi\n        if [ -f \"/service/$serviceName/run\" ]; then\n            cmp -s \"$servicePath/run\" \"/service/$serviceName/run\" > /dev/null\n            if (( $? != 0 )); then\n\t\t\t\tlogMessage \"updating $serviceName run file\"\n\t\t\t\tcp \"$servicePath/run\" \"/service/$serviceName/run\"\n            fi\n\t\t\tif [ -e \"$servicePath/down\" ]; then\n\t\t\t\tsvc -d \"/service/$serviceName\"\n\t\t\telse\n\t\t\t\trm -f \"/service/$serviceName/down\"\n\t\t\t\tif serviceIsUp $serviceName ; then\n\t\t\t\t\techo \"restarting $serviceName service\"\n\t\t\t\t\tsvc -t \"/service/$serviceName\"\n\t\t\t\telse\n\t\t\t\t\tlogMessage \"starting $serviceName service\"\n\t\t\t\t\tsvc -u \"/service/$serviceName\"\n\t\t\t\tfi\n\t\t\tfi\n        fi\n    fi\n}\n"
  },
  {
    "path": "HelperResources/VersionResources",
    "content": "# dummy file to prevent failure with older package setup scripts\n\n# version resources now in EssentialResources"
  },
  {
    "path": "PackageDevelopmentGuidelines.md",
    "content": "# SetupHelper package development\n\n**Kevin Windrem kwindrem@icloud.com**\n\n**updated for v6.0**\n\nThis document provides guidance in developing a package suitable for\nPackageManager management. When this guide is followed, PackageManager\nshould be able to download the package from GitHub or from media\ninserted in the GX device and install it (bring it into operation under\nVenus OS).\n\nPackageManager is part of the SetupHelper package. It monitors GitHub\nfor package updates, downloads and installs them automatically (if\ndesired). It also provides a mechanism to add and remove packages from\nthose PackageManager will manage. The user interface for PackageManager\nis a set of menus in the Venus OS GUI located at Menu / Settings /\nPackageManager.\n\nSetupHelper includes a set of utilities intended to handle the bulk of\npackage installation and uninstallation and also insures the package is\nreinstalled following a firmware update. A Venus OS update will restore\nany modified files to factory defaults, so any modifications must be\nreinstalled following the Venus OS update.\n\nThere are a few basic requirements for a package to be recognized by\nPackageManager:\n\n1.  The package name must conform to unix file naming convention.\n\n    To be most compatible, limit the package name to Upper and lower case\n    alpha characters or numbers. **No spaces are allowed.**\n\n1.  The package must contain a file named `version`. Specifics of the\n    version are described later.\n\n2.  The package should contain a file named `setup`. This should be an\n    executable shell script that will install, uninstall or reinstall\n    the package. SetupHelper includes unix bash shell script\n    extensions that must be included in the setup script in order to\n    be properly managed for reinstall after a Venus OS update. More\n    details on the SetupHelper extension later.\n\n3.  The package should contain a file named `gitHubInfo` which should\n    contain the Git Hub user name and default branch separated by `:`\n    E.g., `kwindrem:latest`\n\n4.  The primary purpose of the setup script is to install modified files\n    to the Venus OS file system. It is important that the package\n    setup script is also written to **uninstall** the package,\n    restoring any modified files to the factory settings. SetupHelper\n    provides a number of utilities for this.\n\n5.  In order for PackageManager to download the package from the\n    internet, it must be contained in a GitHub repository. Any GitHub\n    branch, tag or release may be referenced as long as it is valid.\n    The suggested tag is `latest` which should point to the most\n    recent released version. Additional tags matching a version number\n    may be created in order to allow download of specific versions.\n    Tags or branches such as `beta`, etc may also be created.\n    PackageManager can also use branch names for specific downloads.\n\n6.  The package script is run automatically only if a reinstall is\n    required. Code that needs to run constantly or once each boot\n    should use a service.\n\n7.  A package may modify (replace) files in the Venus OS to enhance or\n    change behavior. These replacement files should be stored in \"file\n    sets\" one for each Venus OS version so that the package can be\n    installed regardless of the Venus OS version currently running.\n    More about file sets later.\n\n8.  A package may optionally restrict its operation to a range of Venus\n    OS versions.\n\n9.  A package may also restrict its operation to a specific platform.\n    Currently, the only restriction is for RaspberryPi platforms.\n\n10. The package may include a ReadMe file describing the package and how\n    to install it.\n\n11. The package may contain a change log indicating what changed in each\n    version.\n\n12. The package may modify GUI files (specifically GUI v1 currently).\n    Normally these modifications are essential to the functionality\n    provided. If GUI v1 is not present on the system, the install will\n    fail. This can be overridden with the `GUI_V1_NOT_REQUIRED` flag\n    file in the package directory.\n\n13. Setup scripts should execute as rapidly as possible. Time spent\n    inside a setup script will delay other activities such as\n    installing other packages and Package Manager will appear to be\n    hung as there is no status update during lengthy operations. For\n    this reason: **Any time-consuming activities such as installing\n    files from the internet or compiling code must be offloaded to a\n    secondary process.**\n\n14. Dependencies on other packages should be identified in the\n    packageDependencies file, described later.\n\n## SetupHelper\n\nSetupHelper oversees the installation, uninstallation or reinstallation\nof the package. The package setup script must call (\"source\")\n**IncludeHelpers**, a shell script extension before doing any work to\ninstall or uninstall the package:\n\n```bash\n#### following line incorporates helper resources into this script\n\nsource \"/data/SetupHelper/HelperResources/IncludeHelpers\"\n\n#### end of lines to include helper resources\n```\n\nIncludeHelpers sources the other helper resources.\n\nThese extensions validate the package for the current Venus OS version\nand platform and builds a file set for the current Venus OS version if\nnecessary.\n\nBeginning with SetupHelper v6.0, file and service and dBus Setting\ninstallation is further simplified. These operations will be based on\nvarious list(s) and contents of the services directory located in the\npackage directory.\n\nSome modifications, such as adding lines to /u-boot/config.txt for\nRaspberry PIs will still require code in the setup script, but in\ngeneral **installFiles**, **installServices** and **addDbusSettings**\nfunctions will handle the bulk of the activities. These can be called\nfrom the setup script or more conveniently, triggered when calling\n**endScript**.\n\nIncluding the following line in the setup script BEFORE sourcing\nIncludeHelpers triggers a default prompt for install or uninstall then\nproceeds with the install/uninstall operations:\n\n```bash\nstandardPromptAndActions='yes'\n```\n\nIn that case, InstallHelpers never returns to the setup script.\n\nIf the `standardPromptAndActions` is not set, the helper resources will\nthen hand control back to the setup script. A prompt for user input is\nthen necessary followed by any processing and installation that can't be\nhandled by the list-based functions mentioned above.\n\nAfter any special processing has been performed, the script should call\nthe '**endScript**' function (in CommonResources) in order to exit the\nscript with the proper exit codes. These exit codes are necessary for\nproper behavior of PackageManager and the automated reinstall operations\nperformed at system boot when the Venus OS is updated.\n\nParameters passed to endScript triggers automatic install/uninstall.\n(described later) For example:\n\n```bash\nendScript INSTALL_FILES INSTALL_SERVICE ADD_DBUS_SETTINGS\n```\n\n**genericSetupScript** located in the SetupHelper directory will install\nany package that does not require in-line prompting, install or\nuninstall code. Simply link to this setup script in that package's\ndirectory or copy it there.\n\n## Patched files\n\nPackage installation has historically replaced files to modify content\nof the active file. Beginning with v8.0, SetupHelper will attempt to\npatch an active file in the fileListPatched list rather that replacing\nit. This allows multiple packages to modify the same file.\n\nFiles to be patched require entries in the PatchSource directory in the\nFileSets directory. They also require an entry in the fileListPatched\nlist. A patch (...patch) file is used during the patch process,\noperating on the active file.\n\nThe original, unmodified active file(...orig) along with a modified\nversion are needed to create the patch file. If these files are present\nin the PatchSource directory, updatePackage will attempt to create a\npatch file, or update it if the orig and modified files have been\nchanged making the .patch file obsolete.\n\n**diff -u** is used to create the patch file.\n\nA ...patchOptions file can override the default -u option. For example\nif the patchOptions file is **-U 0** the patch file will not have any\ncontext lines. If patchOptions includes **MANUAL**, then\n**updatePackage** will not attempt to create a patch file. In this case\nthe patch file will need to be created manually. E.g., with:\n\n```bash\ndiff -u foo-1.orig foo-1 > foo-1.patch\n```\n\nThe patch file then needs to be located in the PatchSource directory.\n\nOn the GX device, helper resources use the .patch file to create a\npatched file. These are stored in a temp directory created by **mktemp**\nand can be referenced with $patchedFiles/foo-1.patch.\n\nSome modifications may not be suitable for all Venus OS versions. The\npatch mechanism allows multiple patch files. E.g.:\n\n```bash\nPageSettings.qm-1.patch, PageSettings.qm-2.patch\n```\n\nAny unique string after the - is an acceptable suffix for the set of\npatch files.\n\nDuring installation, each patch file will be tested and the first one to\nsucceed is used to patch the active file and unpatch it later during\nuninstall.\n\nDuring installation, CommonResources attempts to patch the current\nactive file, then reverse patch it to insure that original can be\nrecreated during a future uninstall. If these succeed, the install is\nallowed to continue.\n\nDuring the uninstall process, the active file replaced by the .orig file\nif no other packages have modified the file. However, if there are other\npackage modifications still present in the active file, it is \"reverse\npatched\" to restore the file to it's state before the package was\ninstalled. This would remove the modifications for this package while\npreserving modifications from other packages.\n\nThere is a slight possibility that this \"reverse patch\" could fail. If\nthis occurs, the active file is replaced by the .orig file in order to\nprevent a system crash due to a missing or incorrectly patched active\nfile. **Modifications from other packages are lost, requiring them to be\nreinstalled.**\n\nThere are restrictions that package authors need to keep in mind. These\nrestrictions are due to \"context\" created for the modification. These\nare lines prior to and after the modification that are the same with and\nwithout the modification. **patch** uses these to locate the proper\nplace in the current active file to apply the patch.\n\n1.  Modifications from multiple packages must not overlap. Even\n    modifications that occur at the exact same place in the file might\n    not produce a patch file that succeeds with and without the other\n    package's modifications because the context lines won't match.\n\n2.  Modifications at the very beginning or end of a file may not produce\n    a usable patch file. Again, because there is insufficient context.\n\nIt is important that the author identify other packages that modify the\nsame files and test installation and uninstallation of all packages\ninvolved, *and* in different install/uninstall orders.\n\n> [!NOTE] \n> The patch utility provided with Venus OS is part of BusyBox\n> and has limitations that prevent its use by SetupHelper. Therefore, a\n> more fully functioned version is included with SetupHelper beginning\n> with v8.0 and is used by HelperResources.\n\n## Setup script command line options\n\nCommonResources checks for optional command line parameters before\npassing control to the body of the setup script.\n\n- **reinstall** indicates this is to be a reinstall. Installed version\n  is compared to the version in the package directory and skips\n  reinstall if they match. User prompting is skipped. (These version\n  checks are now redundant since **reinstallMods** which controls\n  package reinstalls following a Venus OS update also check versions\n  before calling the package setup script.)\n\n- **install** indicates that the prompts should be skipped and the\n installation begun with stored options (more about this later).\n **uninstall** indicates the prompts should be skipped and the package\n uninstalled.\n\n- **auto** silences all console messages. Progress of the setup is\n  logged. Without the **auto** option progress is also written to the\n  console.\n\n- **deferReboot** instructs endScript to skip the automatic system\n  reboot and return indicating a future reboot is needed.\n\n- **deferGuiRestart** is as above but for automatic GUI restarts.\n\nWithout any options, helper resources assumes a user has run the script\nand sets up the environment to prompt for user input to control\nadditional execution.\n\nWhen the setup script regains control following CommonResources, the\nvariable 'scriptAction' will be set to either NONE, INSTALL or\nUNINSTALL. NONE indicates that the script should prompt for user input\nto control further execution. These prompts should be skipped if\nscriptAction is either INSTALL or UNINSTALL. Note that if an install\naction fails, scriptAction will be set to UNINSTALL, so scriptAction\nmust be tested again after the install section and perform the uninstall\noperations. This prevents a partial install from disrupting the system\noperation. Note that this behavior is not automatic and must be written\ninto all package setup scripts.\n\nVenus OS is a \"dual root fs\" system. That is, the operating system and\nexecutable parts of the system reside on one of two root partitions. One\npartition is active and the other inactive. A Venus OS firmware update\ninstalls files to the inactive partition. Then when the update has been\nverified, the active and inactive portions are swapped and the system\nrebooted to execute the updated code. If the update is unsuccessful, the\nswap does not occur and the old executable files continue to run. This\nprevents a partial or corrupted firmware update from bricking the\nsystem. A third partition (/data) holds any persistent information\n(settings, etc).\n\nPackages are stored in the data partition so they survive a Venus OS\nfirmware update, however any system files will be overwritten.\n\nAt boot time, the **reinstallMods** script is called. Starting with\nSetupHelper 8.0, **reinstallMods** only installs the PackageManager\nservice. It sets a flag: /etc/venus/REINSTALL_PACKAGES then exists.\n**PackageManager** then tests this flag file and installs the ALL\npackages, including the remainder of SetupHelper. This was done to avoid\nso that all of PackageManager's pre-install checks to be made prior to\ninstalling a package. Plus some of the packages available now take\nseveral minutes to install resulting in a lag to install all packages\nafter a Venus OS update. Now, status is shown in **PackageManagers**'s\nmenus and in it's log file.\n\nWhen packages are reinstalled, the package setup script is called with\nthe **reinstall**, **auto**, **deferReboot** and **deferGuiRestart**\noptions. **PackageManager** will then trigger a system reboot or GUI\nrestart if any package setup scripts have requested those actions.\n\n**PackageManager** acts on requests for system reboot or GUI restarts\nwhen an install or uninstall is triggered from it's menu. A user choice\nto defer reboots and GUI restarts is then provided.\n\nA package may sometimes need to manually modify certain files because\nthe automatic mechanisms are too general. For example, adding device\ntree overlays to `/u-boot/config.txt` must be done in a way that it does\nnot disturb what's currently in the file. All the automatic install\nmechanism can do is replace the existing file. Code for such\nmodifications goes in the `setupAction == 'INSTALL'` section of code and\nany restoration code would still go in the `setupAction == 'UNINSTALL'`\nsection.\n\nSetup scripts written prior to SetupHelper v6.0 will continue to\nfunction. even if the `INSTALL_...` options are set when calling\nendScript. endScript will attempt to repeat install and uninstall\noperations but no harm will be done (other than taking additional\ntime).\n\n## Automated install\n\nBeginning with SetupHelper v6.0, install and uninstall can usually be\nhandled within CommonResources. Prior to SetupHelper v6.0, the setup\nscript needed code to install and uninstall every modified file or\nservice using the utilities described in the next section. Calls to\ninstall and uninstall services was also needed.\n\nInstalls use \"file lists\" to install modified files. Each file\nmodified is saved in a modified files list which is then used to\nuninstall the package.\n\nRefer to the section on File Sets below for more details on file\nlists.\n\nAll services to be installed by the automatic processes must be\nlocated in the services directory. There is no services list since the\ndirectory provides the necessary information. An installed services\nlist is created to allow for automatic uninstall.\n\n`/data/< package name >/services`\n\nServices are still directories with run and log/run files as before.\nAny service directories found in services will be automatically\ninstalled. Automatically installed services are added to an installed\nservices list and will be automatically uninstalled using that list.\n\nThe name of the directory within services will determine the service\nname. E.g., the following service directory will create the\nPackageManager service\n\n`/data/SetupHelper/services/PackageManager`\n\nPrior to SetupHelper v6.0, services were located in the package\ndirectory. The service name was determined by the package name, or\nspecified on the installService line. These services will NOT be\nautomatically installed, so their service directories are moved to\nservices and named appropriately.\n\nThe modified files and services list are located in /etc/venus.\n\n`/etc/venus/installedModifications/installedFiles-<package name>`\n\n`/etc/venus/installedModifications/installedServices-<package name>`\n\nThis location was chosen because it is removed on a Venus OS update.\nRunning the setup script to uninstall a package will therefore do\nnothing as expected. If the lists were stored in /data, then an\nuninstall would attempt to uninstall the files and services again.\nGenerally, that isn't a problem but really should not happen.\n\nThe package is uninstalled by walking through the installed... lists\nto restore the original files. Generally, those calls in the setup\nscript are no longer necessary.\n\n## SetupHelper utilities\n\nSetupHelper provides a set of utilities to simplify the installation of\nmodified files (called a \"replacement\") into the active root file system. It pulls the correct\nreplacement file from a \"file set\" for the current Venus OS version,\nmoves the original out of the way and installs the replacement file. On\nuninstall, the original file is moved back to the active location (file\nname) leaving the system in an unmodified state.\n\n> [!NOTE]\n> Starting with v6.0, these utilities may be of less use but are still\nincluded.\n\n**updateActiveFile** is a function that installs a replacement file as\ndescribed above. Typically, the replacement file has the same name as\nthe original so the simple form of the command can be used. For example,\nto replace a GUI file:\n\n`updateActiveFile \"/opt/victronenergy/gui/qml/main.qml\"`\n\nIf however, the replacement file has a different file name, a second\nform is used. This may be necessary if the setup script has to build or\nmodify the replacement file.\n\n`updateActiveFile \"$scriptDir/foo.qml\" \"/opt/victronenergy/gui/qml/main.qml\"`\n\n**restoreActiveFile** is a function that undoes the above operation and\nis called during uninstall with the same file names as were used with\nupdateActiveFile during install:\n\n`restoreActiveFile \"/opt/victronenergy/gui/qml/main.qml\"`\n\n**backupActiveFile** is a function that creates the .orig file used by\nrestoreActiveFile but does not update the active file. It is preferable\nto make modifications in the setup script to a temp file then use\n**updateActiveFile** as described above but this is not always possible.\n\nUse only when something modifies the active file in place:\n\n`backupActiveFile \"/etc/pointercal\"`\n\n`ts_calibrate # modifies /etc/pointercal directly`\n\n**installService** is a function that installs and starts a dameon\nservice. The service will be placed in the / service directory (or\n/opt/victronenergy/service for v2.80 or later). A folder in the package\nfolder named 'service' must contain the files that will end up in\n/service under the service name\n\n`installService FooService`\n\nWill copy the service directory from the package directory into\n/service/FooService\n\n**removeService** is a function that removes the service which is\nnecessary during an uninstall to restore the system to factory.\n\n`removeService FooService`\n\n**logMessage** is a function that will log anything of interest.\nMessages are either sent to stdout or to the PackageManager log file:\nlog file: /var/log/PackageManager/current.\nLogging is encouraged as it helps debug systems in\nthe field (and while developing the package). Without the **auto**\noption on the call to the setup script, these messages are also output\nto the console.\n\nTo conform to Victron guidelines, messages are sent to stdout in\nall but a few unavoidable situations:\nrunning the script in **auto** from the command line (discouraged)\n**reinstallMods** or **blindInstall** **blindUninstall**.\n\nWhen scripts are run from PackageManager, stdout is collected\nand forwarded to the log using python's logging.info () method.\nThis way, a the informaiton is persistant for debugging.\n\nWhen scripts are run from the command line, any messages\nappear on the console but are **NOT logged**.\n\n`logMessage \"this text will end up in the log\"`\n\n**endScript** Function to finish up, prompt the user (if not\nreinstalling) and exit the script. (Details are described below.)\n\n*The following functions simplify the task of getting user input.*\n\n**standardActionPrompt** displays a menu of actions and asks the user to\nchoose\n\n- It sets scriptAction accordingly and returns\n\n- It also handles displaying setup and package logs then asks for an\n action again\n\n- It also handles quitting with no action - the function *exits the\n shell script* without returning in this case\n\n- The basic action prompt includes install, reinstall, quit, display\n logs (2 choices)\n\n- A reinstall option is enabled if the optionsSet option exists\n\n- When reinstall is enabled, selecting install, returns a scriptAction\n  of NONE indicating additional prompting may be needed to complete the\n  install\n\n- At the end of these prompts, the main script should set scriptAction\n  to INSTALL\n\n- If reinstall is selected, the script action is set to INSTALL and the\n  main script should then skip additional prompts and allow options set\n  previously to control the install\n\n**yesNoPrompt \"prompt \"**\n\n- Asks the user to answer yes or no to the question\n\n- Any details regarding the question should be output before calling\n  yesNoPrompt\n\n- **yesNoPrompt** sets **$yesResponse** to true if the answer was 0 if\n  yes and 1 for no so that the return code can be checked rather than\n  checking $yesResponse:\n\n    ```bash\n    if yesNoPrompt \"do it (y/n)?\" ; then\n      do stuff for yes response\n    else\n      do stuff for now response\n    fi\n    ```\n\nA set of utilities manages dbus Settings: creating, removing, updating.\nIt is sometimes necessary for the setup script to create dbus Settings\nso GUI has access to them. This is often the case when the package\ndoesn't run its own service.\n\nThe following functions will create dbus settings if they do not already\nexist or update their value if they\n\n```bash\nupdateDbusStringSetting \"/Settings/StringSetting\" \"the new string\" \nupdateDbusIntSetting \"/Settings/IntegerSetting\" 5\nupdateDbusRealSetting \"/Settings/FloatingPointSetting\" 6.0\n```\n\n**setSetting** is a function that updates the value of an existing dbus\nSetting. It is faster than calling one of the above update... functions.\nThe new value can be any data type but strings must be quoted.\n\n```bash\nsetSetting \"/Settings/foo\" \"new string\" \nsetSetting \"/Settings/bar\" 18\n```\n\nThe following function removes the settings. Limit the number of\nsettings to about 20 to avoid some being missed (not sure why). It is\nfaster to remove multiple settings at the same time than it is to call\n'removeDbusSettings' for each one.\n\n```bash\nremoveDbusSettings \"/Settings/foo\" \"/Settings/bar\" \n```\n\n## SetupHelper variables\n\nSetupHelper manages or tests a set of variables that control script\nexecutions:\n\n**$scriptAction** provides direction for the setup script and has the\nfollowing values:\n\n- NONE - setup script should prompt the user for the desired action and\n  set scriptAction accordingly\n\n- EXIT - the setup script should exit immediately\n\n- INSTALL - the setup script should execute code to install the package\n\n- UNINSTALL - the setup script should execute the code to restore the\n  Venus files to stock If installation errors occur within functions in\n  CommonResources, scriptAction will be changed to UNINSTALL.\n\nThe setup script MUST retest scriptAction after all installation code\nhas been executed so the package can be removed, rather than leaving the\nsystem with a partially installed package.\n\n**$rebootNeeded** - true signifies a reboot is required after the\nscript is run. The setup script should set **rebootNeeded** to true if a\nreboot is needed following install/uninstall\n\n*The following variables contain useful information but should not be\nchanged by the setup script:*\n\n- **$scriptDir** - the full path name to the startup script the\n  script\\'s code can use this to identify the location of files stored\n  in the package\n\n- **$scriptName** - the basename of the setup script (\"setup\")\n\n- **$reinstallScriptsList** - the file containing a list of scripts to be\nrun at boot to reinstall packages after a Venus software update (by default, this is\n/data/reinstallScriptsList)\n\n- **$installedVersionFile** - the name of the installed version file\n\n- **$venusVersion** - the version of VenusOS derived from\n/opt/victronenergy/version\n\n- **$fileList** - the version-dependent location for the replacement\nfiles\n\n- **$fileListVersionIndependent** - the location for files that are\nindependent of Venus OS version\n\n- **$fileListPatched** - the location for files that are to be patched\nprior to install\n\n> [!NOTE] \n> Prior to SetupHelper v6.0 version-independent replacement files\nwere in $pkgFileSets directory.\n\n- **$pkgFileSets** - is the location of all file sets\n\n- **$fileSet** - is the location of version-dependent files for the\ncurrent Venus version\n\n- **$runningAtBoot** - true if the script was called from reinstallMods (at boot time) \n \n  signifying this is to be an unattended (automatic) installation\n\n  CommonResoures sets this variable based on command line options\n\n- **$setupOptionsDir** - the location of any files that control installation\n\n  These options are maintained in a separate directory so reinstalling\n  the package does not remove them so that a reinstall can proceed\n  without prompting again\n\n- **$obsoleteVersion** - prevents installation starting with this Venus\nOS version\n\n- **$firstCompatibleVersion** - prevents installation *before* with this\nVenus OS version\n\n## Package lists\n\nIt is usually necessary to create a specific replacement file for\ndifferent Venus OS versions. This allows the package to be installed\nregardless of the Venus version. These different replacement file\nversions are contained in a \"file set\": a directory with the Venus OS\nversion number as it's name. The collection of file sets is stored in\nthe 'FileSets' directory in the package directory.\n\nSome files in a package may not be tied to specific Venus OS versions.\nThese are typically additions to the stock files, and when a single file\ncan be used across all Venus OS versions. Prior to SetupHelper v6.0,\nthese *version-independent* files were contained in the FileSets\ndirectory. Starting with v6.0, they are located in the\n**VersionIndependent** file set.\n\nThese two file lists are kept separate because they are treated\ndifferently.\n\nThe list **fileList** has always been used by helper resources to guide\ninstallation of *version-dependent* files\n\nStarting with v6.0, three additional lists have been added:\n\n**fileListVersionIndependent** lists all *version-independent* files.\nThose files exist in the **VersionIndependent** file set.\n\n**fileListPatched** is a similar list for any replacement files that are\ncreated with the unix patch command. Patch replacements are described\nabove.\n\nThe file lists consist of one line per file with the full path and name\nof the file on each line.\n\nThe **DbusSettingsList** contains a list of dBus Settings to be added to\nthe system as part of this package. Settings are traditionally added via\na service but in cases where the package does not have a related\nservice, this mechanism allows there creation or update from the setup\nscript. Lines in **DbusSettingsList** are in the format:\n\n```json\n{\"path\":\"/Settings/Relay/0/Show\", \"default\":1, \"min\":0, \"max\":1}\n```\n\n\"default\" defines the default value plus the data type (1 for an\ninteger, 1.0 for float, \"something\" for a string)\n\n\"min\" and 'max\" are optional and set the range of acceptable values.\n\nRefer to Victron dbus documentation for more details\n\nFile list for version-dependent files only:\n\n```\n/data/< package name >/FileSets/fileList\n```\n\nFile list for version-independent files:\n\n```\n/data/< package name >/FileSets/fileListVersionIndependent\n```\n\nFile list for patched files:\n\n```\n/data/\\< package name \\>/FileSets/fileListPatched\n```\n\nFile list for dBus Settings:\n\n```\n/data/< package name >/DbusSettingsList\n```\n\n### Missing active file directories\n\nRecently, Victron Energy has changed the name of some directories which contain files requring modificaiton by the package.\nFor example, /opt/victronenergy/bus-generator-starter was renamed /opt/victronenergy/dbus-generator.\n\nIn order to accommodate these name changes, SetupHelper v8.23 checks for the existance of the enclosing directory\nfor an active file and skips the update if the directory does not exist. This is logged but installation is allowed to continue.\n\n[!NOTE]\nDevelopers should review such log entries to insure there are no missing active file updates!\n\nIn such situations **fileList** and **fileListPatched** must include **both** enclosing directories. E.g.,:\n\n```bash\n/opt/victronenergy/dbus-generator-starter/startstop.py\n/opt/victronenergy/dbus-generator/startstop.py\n```\n\n## Version file\n\nA package must contain a version file. This is the *package* version,\nnot the Venus OS version. The package version is used by PackageManager\nto decide if an automatic download is needed by comparing the version\nfrom GitHub with the version stored on the system. Likewise, the stored\nversion is compared to the installed version to trigger an automatic\ninstall.\n\nThe version file is a text file with a single line of the form: v1.2,\nv1.2\\~3 v1.2a3.\n\nVersions that include a \\~ or lettered version are treated as\npre-release.\n\n- 'd' represents a development release\n\n- 'a' represents an alpha release \n- 'b' or '\\~' represents a beta release\n- none of the above represents a released version\n\nVersion numbers are prioritized: 'a' is \"newer\" than 'd', etc.\n\n\"newer\" versions will replace older versions when automatically\ndownloading. Exception: if the branch/ tag set in PackageManger is a\nspecific version (e.g., v.4.6) the stored version must match rather than\nbeing older than. Installs always occur if the versions do not match.\n\n## Restricted install\n\nThe package may optionally contain files that place restrictions on\nwhich Venus OS versions or platforms the package may be installed on. If\nany of these tests fail, the install will also fail!\n\nIf present **obsoleteVersion** identifies the first version that are not\ncompatible with this package. E.g., of obsoleteVersion contains v7.2 and\nthe current Venus OS version if v8.0, then the package can't be\ninstalled.\n\nIf present **firstCompatibleVersion** identifies the first version that\nIS compatible with this package. E.g., if firstCompatibleVersion\ncontains v8.0 and the current Venus OS version is v7.2, the package\ncan't be installed.\nIf **firstCompatibleVersion** is not present, SetupHelper uses v3.10 as the first compatible version.\n\nNote that if both **firstCompatibleVersion** and **obsoleteVersion** are\nincluded in the package directory, the obsoleteVersion must be greater\nthan firstCompatibleVersion.\n\nIf present **validFirmwareVersions** identifies all versions which have been\ntested as compatible with the package. It is a list of Venus OS versions, one per line.\nIf this file is present, **firstCompatibleVersion** and **obsoleteVersion** are redundant.\n\nIf the file **raspberryPiOnly** exists in the package directory, the\nplatform (aka 'machine') MUST be raspberrypi2 or raspberrypi4. If not,\ninstallation will be blocked.\n\nMany packages modify the GUI file system. With the introduction of\ngui-v2, some systems will not have the GUI v1 files in place or will not\nbe running the original GUI.\n\nIf GUI v1 files are required for the package, it's installation will\nfail. In some cases, the GUI modifications are not essential for package\nfunctionality, so if the flag file **GUI_V1_NOT_REQUIRED** is included\nin the package's root directory the package install will not consider\nmissing GUI v1 files an error.\n\nGUI v1 files are those found in /opt/victronenergy/gui. If files from\nthat directory appear in the file list and **GUI_V1_NOT_REQUIRED** is\nnot in the package directory, the install will not be permitted. A check\nis also made in **updateActiveFile** and will force a package uninstall.\n\n**NOTE: SetupHelper will allow an install if the GUI v1 files are\npresent on the system. However, GUI v1 may not currently be running in\nwhich case, the user will not have access to the added/modified menus.**\n\nFailed installs force an UNINSTALL.\n\n## Package conflict management\n\nPrior to SetupHelper v6.12, packages may interact with each other in\nundesirable ways. For example, one package that modifies the same file\nas another will install over the other package, removing the first\npackage's modifications. Uninstalling either package will result in\nthe stock file being used.\n\nSetupHelper v6.12 adds logic to prevent this from happening. If a\npackage attempts to modify the same file as another package, the\ninstall will fail and the package will be uninstalled. Beginning with\nv8.0, multiple packages may be able to modify the same active file.\nSee the section on patching files above.\n\nThe **packageDependencies** file located in the package directory\ndefines basic requirements that would prevent the package from being\ninstalled.\n\nEach line of the file includes a package name and whether that package\nmust be installed or uninstalled in order for the package to be\ninstalled. For example the file for RemoteGPIO might be:\n\n```\nRpiGpioSetup uninstalled\nGuiMods installed\n```\n\nThese lines tell SetupHelper to block the install of RemoteGPIO unless\nRpiGpioSetup is uninstalled and unless GuiMods is installed.\n\nNote that no changes to other package installations occurs so it would\nbe acceptable for the dependency file for RpiGpioSetup to also specify\nthat RemoteGPIO should be uninstalled. This way only one can be\ninstalled but the user has that choice.\n\nThis mechanism is simple but has a drawback: **Package authors must\nmanually check for conflicts and coordinate with other package authors\non how best to address the conflict.**\n\n## endScript\n\nThe **endScript** function must be called at the end of the setup\nscript. It determines the return code used by the caller (like\nPackageManager) to provide the necessary user prompting and to control\nreboot and service restarts.\n\n**endScript** NEVER RETURNS to the caller !\n\nThe actions taken by endScript depend on a number of shell variables set\npreviously and on parameters passed when calling the function:\n\n- The following parameters are passed from the caller. All optional:\n\n  - **INSTALL_FILES** causes endScript to install/uninstall based on fileList, fileListVersionIndependent and fileListPatched lists.\n\n  - **INSTALL_SERVICE** causes endScript to install/uninstall services located in the package services directory.\n  \n  - **ADD_DBUS_SETTINGS** causes endSctipt to perform the file addition or update of dBus Settings based on the DbusSettingsList in the package\n  directory.\n\n- If **$runningAtBoot** is true the script will exit with **EXIT_REBOOT** if **$rebootNeeded** is true otherwise, the script will exit with **EXIT_SUCCESS** on success.\n\n- If **$runningAtBoot** is false (script was run manually), user\n  interaction controls further action If **$rebootNeeded** is true, the\n  function asks if the user wishes to reboot now. If they respond yes,\n  the system will be rebooted. The user may choose to not reboot now if\n  additional installations need to be done first\n\n- If **$rebootNeeded** is false, the function notifies the user of any\n needed actions\n\n- If **$restartGui** is true the gui service will be restarted\n\nStarting with SetupHelper v6.0, other services will be restarted by\nendSctipt if related files are changed with updateActiveFile or\nrestoreActiveFile. Refer to the updateRestartFlags function in\nCommonResources for details.\n\nIf the setup script is run from the command line (no command line\noptions), **endScript** will prompt the user for a reboot or GUI restart\nif one is needed. The user can choose to trigger the action now or wait\nand do it manually later.\n\nHowever, if the script is running autonomously, the action will be\ntriggered from within endScript, *unless* the script was run with\n**deferReboot** or **deferGuiRestart** on the command line. In this\ncase, the action is not performed but the script exits with the\nappropriate exit code. PackageManager and **renstallMods** use the exit\ncode to choose a course of action following all automatic operations.\nFor manual install/uninstall from PackageManger, the user is given the\nchoice to perform the GUI restart or reboot now or do it later. If\ndeferred, a message will be displayed indicating the package isn't fully\nactive (\"reboot needed\").\n\nFor reinstalls following a Venus OS update, **reinstallMods** will\nreboot the system or restart the GUI after installing SetupHelper.\n\nWhen the setup script completes an install operation, **endScript**\nwrites the package version to a file in / etc/venus. **endScript**\ndeletes this file during an uninstall. The installed version file\nwritten to /etc/venus tells PackageManger which version of each package\nis installed and running. It also tells the reinstall mechanism to skip\nSetupHelper reinstall. So reinstall only happens if the installed\nversion file is NOT present or the installed version differs from the\npackage version itself. The latter may be the case if a Venus OS\nfirmware update has occurred.\n\nSome packages may need to reboot in the middle of the installation\nprocess. For example, if an overlay is needed to test for a specific\ncondition, the setup script should install the overly, but skip the\nremaining setup, then set the **runAgain** shell variable before calling\nendScript. endScript then removes the installed version file so the next\nboot will run the package's setup script again.\n\nIf an install operation fails, it sets the shell variable\n**installFailed**. **endScript** will then switch from INSTALL to\nUNINSTALL to insure that all stock files are restored and the system is\nnot left in a partially modified state. **installFailed** is set by most\nutility functions but should also be set inside any code in the setup\nscript that detects a failure. Also, any code should test\n**installFailed** before proceeding with file modifications.\n\n### endScript exit codes\n\nThe following is a list of exit codes returned when endScript exits:\n\n- EXIT_SUCCESS=0 no further action needed\n\n- EXIT_REBOOT=123 system reboot needed\n\n- EXIT_RESTART_GUI=124 GUI restart needed\n\n- EXIT_INCOMPATIBLE_VERSION=254 install failed - version not compatible\n\n- EXIT_INCOMPATIBLE_PLATFORM=253 install failed - platform not compatible\n\n- EXIT_FILE_SET_ERROR=252 install failed - file set problems\n\n- EXIT_OPTIONS_NOT_SET=251 install failed\n\n  - run setup script from command line\n\n- EXIT_RUN_AGAIN=250 partial install\n\n  - run script again after reboot\n\n- EXIT_ROOT_FULL=249 install failed - no room on root\n\n- EXIT_DATA_FULL=248 install failed - no room on /data\n\n- EXIT_NO_GUI_V1=247 install failed - GUI V1 needed\n\n- EXIT_PACKAGE_CONFLICT=246 install of this package blocked by another package\n\n- EXIT_ERROR=255 install failed - unknown error\n\n## PackageManager\n\nPackage Manager includes a set of menus on the GX device menu system\nthat allows the user to view package versions, control automatic package\nupdates and manually install, uninstall, add and remove packages. This\nprovides an alternative to the command line interface for package\nmanagement.\n\nA PackageManager is a python program that runs as a service to do the\nactual work and to interface with the menus.\n\n### Package Manager menu\n\nThe first line of this menu provides status for Package Manager,\nindicating what it is currently doing\n\n**Automatic GitHub downloads** controls if packages are automatically\ndownloaded from GitHub. This occurs if a newer version is available.\n\n- **On** checks GitHub for a package that is newer than what is stored on\nthe system\n\n- **Once** checks GitHub for a package, then downloads are turned off\n\n- **Off** disables GitHub downloads\n\nGitHub versions are refreshed every 10 minutes if auto downloads is\nturned on. If auto downloads are off GitHub versions are refreshed once\nwhen entering the Package Versions menu. A specific package's GitHub\nversion is also refreshed when entering the Package edit menu.\n\nIf auto downloads are off, GitHub versions expire after 10 minutes.\n\n**Auto install packages** controls whether new versions of a package are\nautomatically installed. Some users may wish to have the system\nautomatically download new updates, but install them manually. In this\ncase, automatic GitHub downloads may be turned on and Auto install\npackages turned off.\n\nAuto install packages also influences whether packages transferred from\nSD/USB media are automatically installed or just transferred to local\nstorage\n\n**Active packages** and **Inactive packages** lead to menus described\nbelow\n\n**action to finish install/uninstall** appears of a system reboot or GUI\nrestart has been deferred (see the Package editor menu)\n\n**Backup & restore settings** leads to the menu described later\n\n**microSD / USB** indicates if removable media has been detected and\nallows it to be ejected prior to removal.\n\n**Restart or initialize ...** leads to the menu described below\n\n### Active packages menu\n\nDisplays all active packages, and allows access to editing the package\nsetup\n\nTapping on one of the entries leads to the Package editor menu\n\nVersion information is displayed for each package:\n\n- GitHub shows the version found on GitHub \n- Stored shows the version stored on the GX device \n- Installed shows the version actually installed and running\n\n> [!NOTE] \n> If the GitHub version is not shown, check the GitHub user and\nbranch/tag, or check your internet connection.\n\n### Package editor menu\n\nThis menu facilitates manual install, uninstall, package removal as well\nas changing GitHub access information for the package.\n\n**GitHub user** is the name of the GitHub user authoring the package.\nNormally this won't change.\n\n**GitHub branch or tag** allows you to specify a branch or specific tag.\nThe default (typically **latest**) references the latest released\nversion of the package. You can change this field to try out a beta\nversion or revert to a specific version. Once the GitHub branch is\nchanged, PackageManager will update the GitHub version. If auto\ndownloads are active the new version will be downloaded automatically.\n\nThe status line shows progress of pending operations, conflicts, or\nprompts for further actions.\n\n**Previous** and **Next** step through other packages without leaving\nthis menu.\n\nThe remaining buttons along the bottom of the menu allow for\n**Download**, **Install**, **Uninstall** or **Remove**. These operations\nrequire a confirmation via **Proceed** or **Cancel** in the status line.\n\n**Remove** will remove the package from the active package list and\nreturn it to the inactive packages list. Packages that are of no\ninterest can be removed to keep the active list cleaner.\n\nPackage manager does not allow removing packages unless they are\nuninstalled first.\n\nPackage manager DOES permit uninstalling SetupHelper, however this will\nremove the Package Manager itself. Once removed, the Blind Install\nmechanism will be needed again !!\n\n**Show Conflicts** appears in the status line if package conflicts\nexist. Pressing that shows a list of conflicts and if possible asks if\nthey should be resolved. If they are, **Proceed** and **Cancel** appear.\nPressing **Proceed** will trigger the necessary package installs and\nuninstalls needed to resolve the conflicts\n\nIf an operation requires a system reboot or GUI restart, a message\nappears in the status line. **Now** triggers that operation. **Later**\nhides the notification without acting on it. This can be handy if you\nare performing multiple operations. The notification will appear when\nnavigating to other packages.\n\n### Inactive packages menu\n\nDisplays all INACTIVE packages, i.e., default packages not yet activated\nor manually remove.\n\nThe first entry is always \\\"new\\\" and allows the operator to enter\npackage name, GitHub user and branch/tag from scratch\n\nAdditional lines (if any) are default packages (from the\ndefaultPackageList file)\n\nIf a package is already added to the version list, it will not appear in\nthe Add Package list\n\nTapping on one of the entries leads to the Add package menu\n\n### Add package menu\n\nAllows the package name, **GitHub user** and **GitHub branch or tag** to\nbe entered or changed and the package added to the active packages list.\nThese are the same as described above under Package editor menu.\n\nPrompting for required information is provided on the status line.\n\nPressing **Proceed** initiates the package add. **Cancel** returns to\nthe Inactive Packages menu.\n\nThe package name must be unique or the add operation will fail with a\nprompt indicating to choose a different name.\n\n### Backup & Restore settings menu\n\nSaves settings to the settingsBackup file on removable SD/USB media or\nto local media (`/data/settingsBackup`). restores from same.\n\n`/data/SetupHelper/settingsList` is a complete list of settings saved to\nsettingsBackup. Categories are:\n\n- GuiMods\n- SetupHelper / PackageManager\n- ShutdownManager\n- SOME Victron stock settings in the following sections  \n  - Alarms\n  - Gwacs\n  - DigitalInputs \n  - Generators\n  - Gui\n  - Pump\n  - Relay\n  - System\n  - SystemSetup\n  - Vrmlogger\n\nAdditionally, backup and restore the following to/from removable media\n\n\n- Any logo files in `/data/themes/overlay` \n- Setup script options in `/data/setupOptions`\n\nAll logs stored in `/data/log` are written to logs.zip on removable media\nas part of a backup operation\n\nThe parameters must exist to be saved. The parameters will be created\nand set to the backed up value during a restore.\n\n> [!NOTE]\n> Victron is working on a more comprehensive mechanism but is not\n> working reliably yet. The Package manager backup and restore is\n> temporary and will be removed when the Victron functionality is\n> working\n\n### Package manager restart/initialize menu\n\nThis menu provides a quick way to reboot the system (**Restart**),\nrestart the GUI (**Restart GUI**) or initialize Package manager.\n(**Initialize**). The latter can be used to clean up Package manager's\npersistent storage. Any custom packages added manually or any GitHub\nuser or branch/tag information will be lost.\n\n### USB/SD updates\n\nWhen the GX device is not connected to the internet, a USB flash drive\nor microSD card provides an install/upgrade path. To use the USB update\nprocess\n\n1.  Navigate to the GitHub, repo, click on tags and select the\n    appropriate branch or specific version.\n\n2.  Choose the .tar.gz download link. (Do not download from the Code tab\n    or download the .zip file. These won\\'t work.)\n\n3.  Copy the archive file to a USB memory stick or microSD card. Do NOT\n    unpack the archive\n\n4.  Repeat this for all packages you wish to install. (These can all be\n    placed on the same media along with the SetupHelper blind install\n    `venus-data.tgz` file)\n\n5.  Insert the stick in the GX device.\n\n6.  If SetupHelper has not yet been installed, follow the Blind Install\n    process from the ReadMe.\n\nOnce Package Manager is running, it will transfer and unpack the archive\nfiles and update the package list with the new packages.\n\nIf Auto install packages is turned on, the packages will then be\ninstalled\n\n> [!NOTE]\n> No version checks are made for packages found on SD/USB media!\n> Package Manager is quite content to transfer and install an older\n> version so make sure you have the latest version especially if your GX\n> device does not have internet access.\n\n### Package manager control via removable media\n\nBesides the menus described above, Package manager can be controlled via\n\"flag\" files on removable media. These flag files trigger behavior if\nthey are detected. The file contents is not important, only the\nexistence of the file.\n\n**SETTINGS_AUTO_RESTORE**\n\nAn automatic settings restore will be performed when PackageManager if\nthe file is present.\n\n> [!CAUTION]\n> Leaving this removable media in the system will trigger\n> settings restore with every boot. You must remove the flash drive\n> after auto restore\n\n**AUTO_EJECT**\n\nALL removable media is ejected after the media is scanned AND if after\nall transferrers were performed.\n\nRemovable media can be corrupted if removed while the VRM logger is\nstill writing to it so the drive must be ejected to prevent corruption.\nA manual eject button is included in the PackageManager menu.\n\nUnfortunately, the eject mechanism ejects all removable media, not just\na specific one. The VRM logger automatically uses the first removable\nmedia found so there is no control over it, and the presence of\nAUTO_EJECT will eject the media for the logger also.\n\n**AUTO_INSTALL_PACKAGES**\n\nAll packages will be installed even if the Auto Install menu option is\nturned off. This is generally used only for system deployment (see\nbelow).\n\n**AUTO_UNINSTALL_PACKAGES**\n\nAs above, but will uninstall all packages found in /data. This is useful\nif you do not have command line access and end up with a GUI that is\nunresponsive or just to clean up a system, returning it (almost) to\nfactory defaults. This flag file overrides AUTO_INSTALL_PACKAGES if both\nare present\n\nThe system is rebooted after the uninstall all just to be sure there\\'s\nnothing left behind.\n\n**AUTO_INSTALL**\n\nIf the file AUTO_INSTALL is present in a **package directory**, the\npackage will be installed as if the auto install option is set in the\nPackageManager menu. Version checks are still performed and\nDO_NOT_INSTALL is honored.\n\n**ONE_TIME_INSTALL**\n\nIf the file ONE_TIME_INSTALL is present in a **package directory**, the\npackage is automatically installed even if automatic installs are\ndisabled and the DO_NOT_INSTALL flag is set\n\nONE_TIME_INSTALL is removed when the install is performed to prevent\nrepeated installs. Packages may be deployed with this flag set to insure\nit is installed when a new version is transferred from removable media\nor downloaded from GitHub\n\n**INITIALZE_PACKAGE_MANAGER**\n\nThe PackageManager's persistent storage is rebuilt (see Initialize\nabove)\n\n## updatePackage\n\n**updatePackage** is a unix shell script that runs on the development\ncomputer, not the GX device. It is included in the SetupHelper package.\nThe comments at the top of the script provide additional details.\n\nWindows will not run this script natively. However Windows 10 apparently\nsupports bash:\n\nhttps://www.howtogeek.com/249966/how-to-install-and-use-the-linux-bash-shell-on-windows-10/\n\nWindows developer options must be enabled. In addition, differences in\nend of line between platforms may need to be manually adjusted (cr-lf\nfor Windows) for the script to run properly.\n\nAnother option is to run **updatePackage** on a unix virtual machine or\na Raspberry PI running it's native OS.\n\nIn order to identify changed files, the original file for each\nreplacement must be compared against ALL versions of Venus OS. When\nchanges are detected, a new file set version directory needs to be\ncreated and a new \\...orig file copied from the stock Venus OS file.\nThis work is done on the computer creating the package, not on the GX\ndevice.\n\nI use the raspberry Pi images from\nhttp://updates.victronenergy.com/feeds/venus/ because these contain the\ncompete file system. Alternatively, the file system can be copied from a\nrunning GX device ***prior*** to installing any packages. I create a\ndirectory on the managing computer called OriginalFiles, then create a\ndirectory for each Venus OS version: v2.81, v2.90\\~12, etc.\n\nNext, I copy the /etc, /opt and /var/www/venus/styling directories from\nthe Venus OS image to the OriginalFiles/vX.Y\\~Z directory.\n\nI limit StockFiles to these directories to minimize storage space on the\nsystem used to generate the package files.\n\n99% of the files likely to be modified by a package are located there.\n\nThis is an artificial limit and other parts of the file system may be\nincluded if needed. In order to run the necessary checks, Venus OS\nversions need to be available to the managing computer.\n\nStarting with SetupHelper v5.0, it is recommended that a file set exist\nfor all supported Venus OS versions. This speeds installation time\nbecause it is not necessary to build a file set for a missing Venus OS\nversion. CommonResources will attempt to create a missing file set,\nhowever it may not be possible to create one if version-dependent\noriginal files don't match a file for other file sets.\n\n**updatePackage** runs through all StockFiles version directories and\nall package directories and creates file sets in each package.\n\nBefore running this script, you need to edit the FileSets/fileList\\*\nfiles to include the files your package will modify. Use full path names\nto avoid issues.\n\nFile sets created with **updatePackage** will contain ALL replacement\nfiles (or symbolic links to an identical replacement in another file\nset) in every file set. This change prevents a problem where a matching\noriginal file could not be found even though a file set does exist for\nthe current Venus OS version. This resulted in an \"incomplete file set\"\nerror and failure to install the package. To clear the error, it was\nnecessary to reinstall the package and/or the Venus OS firmware. Prior\nto v5.0, these symbolic links were missing, and in fact entire file sets\nmight be missing if they can be created from other Venus OS versions. It\nwas then necessary for **\\_checkFileSets** to fill in the missing files\nfor the current Venus OS version.\n\nStarting with SetupHelper v6.0, **updatePackage** will relocate all\nversion-independent files included in the fileListVersionIndependent\nlist. Prior to v6.0, these files were located in the FileSets directory.\nStarting with v6.0, these files are located in the VersionIndependent\nfile set\n\n**updatePackage** provides an option to relocate alternate original\nfiles (described below) to their new locations: AlternateOriginals. If\nAlternateOriginals is already present, the move will be automatic.\n\n**checkFileSets** in CommonResources checks for a COMPLETE flag file\nbefore attempting to fill in a missing file set. This saves time since a\nsearch is not needed for all replacement files. (In previous versions of\nSetupHelper, a test was made for all replacement files. This should\nsucceed but takes time.)\n\nFile sets created with **updatePackage** from an older version of\nSetupHelper only contain replacement files when the original file\nchanges between versions. Older file sets will also not contain the\nCOMPLETE flag file. Also, the package may not contain a file set for the\ncurrent Venus OS version. **checkFileSets** will attempt to fill in\nmissing files or build a missing file set by comparing the original\nfiles in other file sets with the active file installed by Venus OS. If\na match is found, this means the replacement file from the other file\nset also applies to the current Venus OS version.\n\nAfter running this script, you may find file sets populated with\n`...NO_REPLACEMENT` files. These indicate where you need to create\nreplacement files for your packages. You will need to add your changes\nto each replacement file in each file set.\n\nNaming:\n\n- the replacement file has the extension of the actual file, e.g.,\nPageMain.qml\n\n- the original file adds a .orig extension, e.g, PageMain.qml.orig\n\n- if no original exists, an empty file with the `.NO_ORIG` file will be\ncreated. e.g., `PageMain.qml.NO_ORIG`\n\nExistence of a `.NO_ORG` file after running updateFileSets indicates a\nsignificant problem. What this says is that the replacement file has no\nequivalent in a stock system. If the replacement file is the same for\nall Venus OS versions, simply remove it from fileList and place the\nreplacement in the FileSets directory, not in a version directory.\n\nHowever, if the replacement file differs between Venus OS versions, an\nalternate original file needs to be used as a reference. For example, if\nyou are creating a new file `PageMainEnhanced.qml`, then you can probably\nuse `PageMain.qml` as the alternate original. Create a file in FileSets\nnamed `PageMainEnhanced.qml.ALT_ORIG` with a single line with the full\npath to the alternate original:\n\n`/opt/ victronenergy/qui/qml/PageMain.qml`\n\nSometimes, a replacement file is needed in SOME versions of Venus OS but\nin others. An empty file in the file set will instruct SetupHelper to\nuse the orig, e.g., `PageMain.qml.USE_ORIGINAL`\n\n**Starting with SetupHelper v6.0**, alternate original files can optionally\nbe stored in the alternateOriginals directory in the FileSets directory.\nThis removes clutter from the FileSets directory but the functionality\nis the same.\n\n## Blind Install\n\nBy far, the easiest way to install SetupHelper is the \\\"blind install\"\nwhich requires no command-line interaction.\n\n1. Download [venus-data.tgz](https://github.com/kwindrem/SetupHelper/raw/main/venus-data.tgz) from the SetupHelper GitHub repo.\n\n    > **Note:** Mac OS and Safari are set by default to unzip packages. The Open\n    > \"safe\" files after downloading (bottom of Safari Preferences\n    > General) must be disabled in order to retain the zip file.\n\n2.  copy it to the root of a freshly formatted SD card or USB memory\n    stick\n\n3.  place the media in the GX device (Cerbo, CCGX, etc)\n\n4.  reboot the GX device and allow the system to display the GUI\n\n    > If you are running Venus OS v2.90 and beyond you should find the\n    > Package Manager menu at the bottom of the Settings menu\n\n5.  you should remove the media at this point. Mechanisms are in place\n    to prevent reinstallation, but removal is still a good idea\n\nIf you are running Venus OS **prior to v2.90**, perform these *additional\nsteps*:\n\n6.  Reboot the GX device a second time\n\n7.  WHILE the GX device is booting, **REMOVE THE MEDIA** from the GX device *to prevent the next reboot from starting the process all over again*. Failure to do so could disable reinstalls following a Venus OS firmware update !!!\n\nYou should find the Package Manager menu at the bottom of the Settings menu\n\nvenus-data.tgz is available here:\n\nhttps://github.com/kwindrem/SetupHelper/raw/main/venus-data.tgz\n\n> [!CAUTION]\n> Prior to v2,90, this mechanism overwrites /data/rcS.local!\n>\n> If you are using rcS.local to perform boot-time activities,\n> /data/rcS.local must be recreated following this \\\"blind\\\" install\n>\n> Note that SetupHelper also uses /data/rcS.local for reinstallation\n> following a firmware update so use caution in recreating rcS.local.\n\n## Blind UNINSTALL\n\nA blind uninstall mechanism is provided to recover a system with an\nunresponsive GUI (white screen) or no ssh/terminal access. This will run\nall package setup scripts to uninstall that package from system files.\n\nThe archive for this is named `venus-data.UninstallPackages.tar.gz`.\n\n1.  Copy `venus-data.UninstallPackages.tar.gz` to a USB memory stick or SD\n    card\n\n2.  Rename the copy to `venus-data.tar.gz`\n\n3.  Insert the removable media into the GX device\n\n4.  Reboot, wait 2 minutes and reboot a second time\n\n5.  when the system automatically reboots after the second manual one,\n    remove the media\n\nYou should eventually see the GUI on the local display if there is one\nor be able to connect via remote console.\n\n> [!CAUTION]\n> Removing media or power cycling the GX device during the\n> uninstall, especially if reinstalling firmware could render the system\n> unresponsive!\n>\n> Wait to see the GUI before removing media or power cycling.\n\nIn addition to uninstalling all packages, the blind uninstall can\noptionally reinstall VenusOS. To do so, include a `.swu` file for the\nplatform and desired firmware version on the removable media containing\nthe blind uninstall `venus-data.tar.gz` file.\n\nNote that a firmware update can take several minutes to complete but\nwill eventually reboot.\n\nWhen the blind uninstall finishes, `venus-data-tar.gz` file on the\nremovable media is renamed to `venus-data.UninstallPackages.tar.gz` so\nthat the blind install will run only once. This renaming is necessary to\nprevent a loop where the system uninstalls and reboots.\n\n## System automatic configuration and package installation\n\nIt is possible to use SetupHelper to set up a new system based on a\ntemplate saved from a working system.\n\n1.  Setup the working system the way you want the new system to behave\n    including custom icons,\n\n2.  Perform a Settings backup.\n\n3.  Remove the flash drive from the GX device and plug into a computer\n    that has internet access.\n\n4.  Copy venus-data.tgz from the SetupHelper GitHub repo to the same\n    flash drive.\n\n5.  If you wish packages to also be installed, copy the package\n    -latest.tgz file from those repos as well.\n\n6.  Create SETTINGS_AUTO_RESTORE on the flash drive (contents don\\'t\n    matter - file may be empty).\n\n7.  Create AUTO_INSTALL_PACKAGES on the flash drive as well.\n\n8.  Place the flash drive into the GX device to be configured and reboot\n    (once for v2.90 or twice for prior versions).\n\n9.  REMOVE THE FLASH DRIVE after you have verified that all packages\n    have been installed (check Active packages in PackageManager).\n\n## System recovery\n\nIt is unlikely, but some users have reported a package install leaving\ntheir system unresponsive or with a nonfuncitonal GUI (white screen). In this case, your options depend on the current state of the system.\n\nTry the following in this order:\n\n- Reboot. This may clear the problem.\n\n- If you have a functioning GUI (either locally or via remote console, see\nif you can access the PackageManager menu.\n\n  - If so, you can remove packages one at a time from there.\n\n  - If you find an offending package, post an issue to the GitHub repo for\nthat package and include:\n\n    - Platform (Cerbo, CCGX, Raspberry PI, etc)\n    - Venus OS firmware version\n    - Run a Settings backup and post the logs.zip file on the removble media.\n\n  - Remove SetupHelper last since once you do, you loose the PackageManager\nmenus!\n\n- If you have terminal or ssh access, try running the package setup\nscripts to uninstall packages one at a time.\n\n- Try reinstalling Venus OS (firmware):\n\n    - Boot to the previous Venus OS version (in Stored backup firmware),\n     then perform a fresh Online firmware update to the latest version or\n     use the .swu update via removable media.\n    \n    - If you have GUI access, use the Settings / Firmware / Stored backup\n     firmware menu.\n    \n    - If you don't have GUI access, you can also switch to the backup\n     version from the command line:\n    \n       `/opt/victronenergy/swupdate-scripts/set-version.sh 2`\n    \n    - You can also force a firmware update from the command line if you have\n      ssh or terminal access:\n    \n      - For on-line updates:\n    \n        `/opt/victronenergy/swupdate-scripts/check-swupdate.sh -force -update`\n    \n      - For updates from removable media:\n    \n       `/opt/victronenergy/swupdate-scripts/check-swupdate.sh -force -update -offline`\n\n- If PackageManager is still running, it will detect a file named\nAUTO_UNINSTALL_PACKAGES on removable media.\n\n  - Create a file of that name (no extension, content unimportant) on a\n USB memory stick or SD card and insert this into the GX device.\n\n  - The system should eventually reboot. In most cases, this should occur\n    within 1-2 minutes.\n\n  - After reboot, the system should come up in the stock configuration\n    with no packages installed.\n\n  - If the system does not reboot, it is likely PackageManager is no\n    longer running, so try other options.\n\n  - Remember to remove the media containing the AUTO_UNINSTALL_PACKAGES\n    file to this will be repeated the next time PackageManager runs.\n\n- Perform the Blind uninstall procedure above.\n\n- If you are running on a Raspberry PI, you can reimage the system SD\ncard.\n\n- If you have a Cerbo, you can reimage it using this procedure:\n\n  <https://community.victronenergy.com/questions/204255/cerbo-gx-bricked-how-to-recover.html>\n\n  > **Note:** this will wipe out all settings and you\\'ll need to reconfigure\nthe GX device from scratch.\n\nThe Victron \"restore factory default\" procedure can be used to will\nwipe out all settings. You'll need to reconfigure the GX device from\nscratch.\n\nHowever, it will NOT replace the operating system and Victron\napplication, nor will it uninstall any packages.\n\nYou will most likely be locked out of ssh access since log-in\ninformation and ssh keys are stored in the /data partition which is\ncompletely erased by this procedure.\n\nFor this reason, I do not recommend using this as part of your attempt\nto recover a system with no GUI.\n"
  },
  {
    "path": "PackageManager.py",
    "content": "#!/usr/bin/env python\n#!/usr/bin/env python\n#\n#\tPackageManager.py\n#\tKevin Windrem\n#\n#\n# This program is responsible for\n#\tdownloading, installing and unstalling packages\n# \tpackage monitor also checks SD cards and USB sticks for package archives\n#\t\teither automatically or manually via the GUI\n#\tproviding the user with status on installed packages and any updates via the GUI\n#\n# It runs as /service/PackageManager\n#\n# Persistent storage for packageManager is stored in dbus Settings:\n#\n#\tcom.victronenergy.Settings parameters for each package:\n#\t\t/Settings/PackageManager/n/PackageName\t\tcan not be edited by the GUI\n#\t\t/Settings/PackageManager/n/GitHubUser\t\tcan be edited by the GUI\n#\t\t/Settings/PackageManager/n/GitHubBranch\t\tcan be edited by the GUI\n#\t\t/Settings/PackageManager/Count\t\t\t\tthe number of ACTIVE packages (0 <= n < Count)\n#\t\t/Settings/PackageManager/Edit/...\t\t\tGUI edit package set - all fields editable\n#\n#\t\t/Settings/PackageManager/GitHubAutoDownload \tset by the GUI to control automatic updates from GitHub\n#\t\t\t0 - no GitHub auto downloads (version checks still occur)\n#\t\t\t1 - updates enabled - one download update every 10 seconds, then one download every 10 minutes\n#\t\t\t3 - one update pass at the fast rate, then to no updates\n#\t\t\t\tchanging to one of the fast scans, starts from the first package\n\nAUTO_DOWNLOADS_OFF = 0\nNORMAL_DOWNLOAD = 1\nHOURLY_DOWNLOAD = 2\nDAILY_DOWNLOAD = 3\nONE_DOWNLOAD = 99\n\n#\t\t/Settings/PackageManager/AutoInstall\n#\t\t\t0 - no automatic install\n#\t\t\t1 - automatic install after download from GitHub or SD/USB\n#\n# Additional (volatile) parameters linking packageManager and the GUI are provided in a separate dbus service:\n#\n#\tcom.victronenergy.packageManager parameters\n#\t\t/Package/n/GitHubVersion \t\t\t\t\tfrom GitHub\n#\t\t/Package/n/PackageVersion \t\t\t\t\tfrom /data <packageName>/version from the package directory\n#\t\t/Package/n/InstalledVersion \t\t\t\tfrom /etc/venus/isInstalled-<packageName>\n#\t\t/Package/n/Incompatible\t\t\t\t\t\tindicates the reason the package not compatible with the system\n#\t\t\t\t\t\t\t\t\t\t\t\t\t\"\" if compatible\n#\t\t\t\t\t\t\t\t\t\t\t\t\tany other text if not compatible\n#\t\t/Package/n/FileSetOK\t\t\t\t\t\tindicates if the file set for the current version is usable\n#\t\t\t\t\t\t\t\t\t\t\t\t\tbased on the INCOMPLETE flag in the file set\n#\t\t\t\t\t\t\t\t\t\t\t\t\tthe GUI uses this to enable/disable the Install button\n#\t\t/Package/n/PackageConflicts\t\t\t\t\t (\\n separated) list of reasons for a package conflict (\\n separated)\n#\n#\t\tfor both Settings and the the dbus service:\n#\t\t\tn is a 0-based section used to reference a specific package\n#\n#\ta list of default packages that are not in the main package list\n#\tthese sets are used by the GUI to display a list of packages to be added to the system\n#\tfilled in from /data/SetupHelper/defaultPackageList, but eliminating any packages already in /data\n#\tthe first entry (m = 0) is \"new\" - for a new package\n#\t\"new\" just displays in the packages to add list in the GUI\n#\tall package additions are done through /Settings/PackageManager/Edit/...\n#\t\t/Default/m/PackageName\n#\t\t/Default/m/GitHubUser\n#\t\t/Default/m/GitHubBranch\n#\t\t/DefaultCount\t\t\t\t\tthe number of default packages\n#\n#\t\tm is a 0-based section used to referene a specific default paclage\n#\n#\t\t/GuiEditAction is a text string representing the action\n#\t\t  set by the GUI to trigger an action in PackageManager\n#\t\t\t'install' - install package from /data to the Venus working directories\n#\t\t\t'uninstall' - uninstall package from the working directories\n#\t\t\t'download\" - download package from GutHub to /data\n#\t\t\t'add' - add package to package list (after GUI sets .../Edit/...\n#\t\t\t'remove' - remove package from list TBD ?????\n# \t\t \t'reboot' - reboot\n#\t\t\t'restartGui' - restart the GUI\n#\t\t\t'INITIALIZE' - install PackageManager's persistent storage (dbus Settings)\n#\t\t\t\t\t\tso that the storage will be rebuilt when PackageManager restarts\n#\t\t\t\t\t\tPackageManager will exit when this command is received\n#\t\t\t'RESTART_PM' - restart PackageManager\n#\t\t\t'gitHubScan' - trigger GitHub version update\n#\t\t\t\t\t\tsent when entering the package edit menu or when changing packages within that menu\n#\t\t\t\t\t\talso used to trigger a Git Hub version refresh of all packages when entering the Active packages menu\n#\n#\t\tthe GUI must wait for PackageManager to signal completion of one operation before initiating another\n#\n#\t\t  set by PackageManager when the task is complete\n#\t\treturn codes - set by PackageManager\n#\t\t\t'' - action completed without errors (idle)\n#\t\t\t'ERROR' - error during action - error reported in /GuiEditStatus:\n#\t\t\t\tunknown error\n#\t\t\t\tnot compatible with this version\n#\t\t\t\tnot compatible with this platform\n#\t\t\t\tno options present - must install from command line\n#\t\t\t\tGUI choices: OK - closes \"dialog\"\n#\n#\tthe following service parameters control settings backup and restore\n#\t\t/BackupMediaAvailable\t\tTrue if suitable SD/USB media is detected by PackageManager\n#\t\t/BackupSettingsFileExist\tTrue if PackageManager detected a settings backup file\n#\t\t/BackupSettingsLocalFileExist\tTrue if PackageManager detected a settings backup file in /data\n#\t\t/BackupProgress\t\t\t\tused to trigger and provide status of an operation\n#\t\t\t\t\t\t\t\t\t0 nothing happening - set by PackageManager when operaiton completes\n#\t\t\t\t\t\t\t\t\t1 set by the GUI to trigger a backup operation media\n#\t\t\t\t\t\t\t\t\t2 set by the GUI to trigger a restore operation media\n#\t\t\t\t\t\t\t\t\t3 set by PackageManager to indicate a backup to media is in progress\n#\t\t\t\t\t\t\t\t\t4 set by PackageManager to indicate a restore from media is in progress\n#\t\t\t\t\t\t\t\t\t21 set by the GUI to trigger a backup operation to /data\n#\t\t\t\t\t\t\t\t\t22 set by the GUI to trigger a restore operation from /data\n#\t\t\t\t\t\t\t\t\t23 set by PackageManager to indicate a backup is in progress to /data\n#\t\t\t\t\t\t\t\t\t24 set by PackageManager to indicate a restore from /data is in progress\n#\n# setup script return codes\nEXIT_SUCCESS =\t\t\t\t0\nEXIT_REBOOT =\t\t\t\t123\nEXIT_RESTART_GUI =\t\t\t124\nEXIT_INCOMPATIBLE_VERSION =\t254\nEXIT_INCOMPATIBLE_PLATFORM = 253\nEXIT_FILE_SET_ERROR\t=\t\t252\nEXIT_OPTIONS_NOT_SET =\t\t251\nEXIT_RUN_AGAIN = \t\t\t250\nEXIT_ROOT_FULL =\t\t\t249\nEXIT_DATA_FULL =\t\t\t248\nEXIT_NO_GUI_V1 =\t\t\t247\nEXIT_PACKAGE_CONFLICT =\t\t246\nEXIT_PATCH_ERROR =\t\t\t245\nEXIT_ERROR =\t\t\t\t255 # generic error\n#\n#\n#\t\t/GuiEditStatus \t\t\t\ta text message to report edit status to the GUI\n#\n#\t\t/PmStatus\t\t\t\t\tas above for main Package Manager status\n#\n#\t\t/MediaUpdateStatus\t\t\tas above for SD/USB media transfers\n#\n#\t\t/Platform\t\t\t\t\ta translated version of the platform (aka machine)\n#\t\t\t\t\t\t\t\t\tmachine\t\t\tPlatform\n#\t\t\t\t\t\t\t\t\tccgx\t\t\tCCGX\n#\t\t\t\t\t\t\t\t\teinstein\t\tCerbo GX\n#\t\t\t\t\t\t\t\t\tcerbosgx\t\tCerbo SGX\n#\t\t\t\t\t\t\t\t\tbealglebone\t\tVenus GX\n#\t\t\t\t\t\t\t\t\tcanvu500\t\tCanVu 500\n#\t\t\t\t\t\t\t\t\tnanopi\t\t\tMulti/Easy Solar GX\n#\t\t\t\t\t\t\t\t\traspberrypi2\tRaspberry Pi 2/3\n#\t\t\t\t\t\t\t\t\traspberrypi4\tRaspberry Pi 4\n#\t\t\t\t\t\t\t\t\tekrano\t\t\tEkrano GX\n#\n#\t\t/ActionNeeded\t\t\t\tinforms GUI if further action is needed following a manual operation\n#\t\t\t\t\t\t\t\t\tthe operator has the option to defer reboots and GUI restarts (by choosing \"Later\")\n#\t\t\t''\t\t\t\tno action needed\n#\t\t\t'reboot'\t\treboot needed\n#\t\t\t'guiRestart'\tGUI restart needed\n#\n#\t\t\tthe GUI can respond by setting /GuiEditAction to 'reboot' or 'restartGui'\n#\n# /Settings/PackageVersion/Edit/ is a section for the GUI to provide information about the a new package to be added\n#\n# /data/SetupHelper/defaultPackageList provides an initial list of packages\n#\tIt contains a row for each package with the following information:\n#\t\tpackageName gitHubUser gitHubBranch\n#\tIf present, packages listed will be ADDED to the package list in /Settings\n#\texisting dbus Settings (GitHubUser and GitHubBranch) will not be changed\n#\n#\tthis file is read at program start\n#\n# Package information is stored in the /data/<packageName> directory\n#\n# A version file within that directory identifies the version of that package stored on disk\n#\tbut not necessarily installed\n#\n# When a package is installed, the version in the package directory is written to an \"installed version\" file:\n#\t\t/etc/venus/installedVersion-<packageName>\n#\tthis file does not exist if the package is not installed\n#\t/etc/venus is chosen to store the installed version because it does NOT survive a firmware update\n#\tthis will trigger an automatic reinstall following a firmware update\n#\n# InstalledVersion is displayed to the user and used for tests for automatic updates\n#\n# GitHubVersion is read from the internet if a connection exists.\n#\tOnce the GitHub versions have been refreshed for all packages,\n#\t\tthe rate of refresh is reduced so that all packages are refreshed every 10 minutes.\n#\t\tSo if there are 10 packages, one refresh occurs every 60 seconds\n#\tAddition of a package or change in GitHubUser or GitHubBranch will trigger a fast\n#\t\tupdate of GitHub versions\n#\tIf the package on GitHub can't be accessed, GitHubVersion will be blank\n#\tThe GUI's Package editor menu will refresh the GitHub version of the current package\n#\t\twhen navagating to a new package. This insures the displayed version isn't out of date\n#\tGitHub version information is erased 10 minutes after it was last refreshed.\n#\tEntering the Package versions menu wil trigger a fast refresh, again to insure the info is up to date.\n#\n#\n# PackageManager downloads packages from GitHub based on the GitHub version and package (stored) versions:\n#\tif the GitHub branch is a specific version, automatic download occurs if the versions differ\n#\t\totherwise the GitHub version must be newer.\n#\tthe archive file is unpacked to a directory in /data named\n# \t\t <packageName>-<gitHubBranch>.tar.gz, then moved to /data/<packageName>, replacing the original\n#\n# PackageManager automatically installs the stored verion if the package (stored) and installed versions differ\n#\n# Automatic downloads and installs can be enabled separately.\n#\tDownloads checks can occur all the time, run once or be disabled\n#\n# Package reinstalls following a firmware update are handled as follows:\n#\tDuring system boot, reinstallMods reinstalls SetupHelper if needed\n#\t\tit then sets /etc/venus/REINSTALL_PACKAGES\n# \tPackageManager tests this flag and if set, will reinstall all packages\n#\t\teven if automatic installs are disabled.\n#\n# Manual downloads and installs triggered from the GUI ignore version checks completely\n#\n#\tIn this context, \"install\" means replacing the working copy of Venus OS files with the modified ones\n#\t\tor adding new files and system services\n#\n#\tUninstalling means replacing the original Venus OS files to their working locations\n#\n#\tRemoved packages won't be checked for automatic install or download\n#\t\tand do not appear in the active package list in the GUI\n#\n#\tOperations that take signficant time are handled in separate threads, decoupled from the package list\n#\t\tOperaitons are placed on a queue with all the information a processing routine needs\n#\n#\tAll operations that scan the package list must do so surrounded by\n#\t\tDbusIf.LOCK () and DbusIf.UNLOCK ()\n#\t\tand must not consume significant time: no sleeping or actions taking seconds or minutes !!!!\n#\t\tinformation extracted from the package list must be used within LOCK / UNLOCK to insure\n#\t\t\tthat data is not changed by another thread.\n#\n# PackageManager manages flag files in the package's setup options folder:\n#\tDO_NOT_AUTO_INSTALL\t\t\tindicates the package was manually removed and PackageManager should not\n#\t\t\t\t\t\t\t\tautomatically install it\n#\tDO_NOT_AUTO_ADD\t\t\t\tindicates the package was manually removed and PackageManager should not\n#\t\t\t\t\t\t\t\tautomaticlly add it\n#\tFORCE_REMOVE\t\t\t\tinstructs PackageManager to remove the package from active packages list\n#\t\t\t\t\t\t\t\tUsed rarely, only case is GuiMods setup forcing GeneratorConnector to be removed\n#\t\t\t\t\t\t\t\tthis is done only at boot time.\n#\n#\tthese flags are stored in /data/setupOptions/<packageName> which is non-volatile\n#\t\tand survives a package download and firmware updates\n#\n# PackageManager checks removable media (SD cards and USB sticks) for package upgrades or even as a new package\n#\tFile names must be in one of the following forms:\n#\t\t<packageName>-<gitHubBranch or version>.tar.gz\n#\t\t<packageName>-install.tar.gz\n#\tThe <packageName> portion determines where the package will be stored in /data\n#\t\tand will be used as the package name when the package is added to the package list in Settings\n#\n#\tIf all criteria are met, the archive is unpacked and the resulting directory replaces /data/<packageName>\n#\t\tif not, the unpacked archive directory is deleted\n#\n#\n#\tPackageManager scans /data looking for new packages\n#\t\tdirectory names must not appear to be an archive\n#\t\t\t(include a GitHub branch or version number) (see rejectList below for specifics)\n#\t\tthe directory must contain a valid version\n#\t\tthe package must not have been manually removed (DO_NOT_AUTO_ADD flag file set)\n#\t\tthe file name must be unique to all existing packages\n#\n#\t\tA new, verified package will be added to the package list and be ready for\n#\t\t\tmanual and automtic updates, installs, uninstalls\n#\n#\t\tThis mechanism handles archives extracted from SD/USB media\n#\n#\n#\tPackages may optionally include the gitHubInfo file containg GitHub user and branch\n#\t\tgitHubInfo should have a single line of the form: gitHubUser:gitHubBranch, e.g, kwindrem:latest\n#\t\tgitHubUser and gitHubBranch are set from the file's content when it is added to the package list\n#\t\tif the package is already in the package list, gitHubInfo is ignored\n#\t\tif no GitHub information is contained in the package, \n#\t\tan attempt is made to extract it from the defaultPackages list\n#\t\tfailing that, the user must add it manually via the GUI\n#\t\twithout the GitHub info, no automatic or manual downloads are possible\n#\n#\t\talternate user / branch info can be entered for a package\n#\t\tuser info probalby should not change\n#\t\tbranch info can be changed however to access specific tags/releases\n#\t\tor other branches (e.g., a beta test branch)\n#\n#\tPackageManager has a mechnism for backing up and restoring settings:\n#\t\tSOME dbus Settings\n#\t\tcustom icons\n#\t\tbacking up gui, SetupHelper and PackageManager logs\n#\n#\tPackageManager checks for several flag files on removable media:\n#\t\tSETTINGS_AUTO_RESTORE\n#\t\t\tTriggers automatically restore dbus Settings and custom icons\n#\t\t\tA previous settings backup operation must have been performed\n#\t\t\tThis creates a settingsBackup fiile and icons folder on the removable media\n#\t\t\tthat is used by settings restore (manual or triggered by this flag\n#\n#\t\tAUTO_INSTALL_PACKAGES\n#\t\t\tIf present on the media, any packages found on the media will be automatically installed\n#\n#\t\tAUTO_UNINSTALL_PACKAGES\n#\t\t\tAs above but uninstalls INCLUDING SetupHelper !!!!\n#\t\t\tOnly applies if present on media (not in /data or a package direcory)\n#\n#\t\tAUTO_INSTALL\n#\t\t\tIf present in a package directory, the package is installed\n#\t\t\t\teven if the automatic installs are disabled in the PackageManager menu\n#\t\t\t\tDO_NOT_AUTO_INSTALL overrides this flag\n#\n#\t\tONE_TIME_INSTALL\n#\t\t\tIf present in a package directory, the package is automatically installed\n#\t\t\t\teven if automatic installs are diabled and the DO_NOT_AUTO_INSTALL flag is set\n#\t\t\tThis flag file is removed when the install is performed\n#\t\t\t\tto prevent repeated installs\n#\t\t\tPackages may be deployed with this flag set to insure it is installed\n#\t\t\t\twhen a new version is transferred from removable media or downloaded from GitHub\n#\n#\t\tAUTO_EJECT\n#\t\t\tIf present, all removable media is ejected after related \"automatic\" work is finished\n#\n#\t\tINITIALIZE_PACKAGE_MANAGER\n#\t\t\tIf present, the PackageManager's persistent storage (dbus Settings parameters) are initialized\n#\t\t\tand PackageManager restarted\n#\t\t\tOn restart, PackageManager will rebuild the dbus Settings from packages found in /data\n#\t\t\tOnly custom Git Hub user and branch information is lost.\n#\n#\t\tA menu item with the same function as INITIALIZE_PACKAGE_MANAGER is also provided\n#\n# classes/instances:\n#\tAddRemoveClass\n#\t\tAddRemove runs as a separate thread\n#\n#\tDbusIfClass\n#\t\tDbusIf\n#\n#\tPackageClass\n#\t\tPackageList [] one per package\n#\n#\tUpdateGitHubVersionClass\n#\t\tUpdateGitHubVersion runs as a separate thread\n#\n#\tDownloadGitHubPackagesClass\n#\t\tDownloadGitHub runs as a separate thread\n#\n#\tInstallPackagesClass\n#\t\tInstallPackages runs as a separate thread\n#\n#\tMediaScanClass\n#\t\tMediaScan runs as a separate thread\n#\n# global methods:\n#\tPushAction () \n#\tVersionToNumber ()\n#\tLocatePackagePath ()\n#\tAutoRebootCheck ()\n\nimport platform\nimport argparse\nimport logging\n\n# constants for logging levels:\nCRITICAL = 50\nERROR = 40\nWARNING = 30\nINFO = 20\nDEBUG = 10\n\nimport sys\nimport signal\nimport subprocess\nimport threading\nimport os\nimport shutil\nimport dbus\nimport time\nimport re\nimport glob\nimport queue\nfrom gi.repository import GLib\n# add the path to our own packages for import\nsys.path.insert(1, \"/data/SetupHelper/velib_python\")\nfrom vedbus import VeDbusService\nfrom settingsdevice import SettingsDevice\n\n\nglobal DownloadGitHub\nglobal InstallPackages\nglobal AddRemove\nglobal MediaScan\nglobal DbusIf\nglobal Platform\nglobal VenusVersion\nglobal VenusVersionNumber\nglobal SystemReboot\t# initialized/used in main, set in mainloop\nglobal GuiRestart\t# initialized in main, set in PushAction and InstallPackage, used in mainloop\nglobal WaitForGitHubVersions # initialized in main, set in UpdateGitHubVersion used in mainLoop \nglobal InitializePackageManager # initialized/used in main, set in PushAction, MediaScan run, used in mainloop\n\n\n# convert a version string to an integer to make comparisions easier\n# the Victron format for version numbers is: vX.Y~Z-large-W\n# the ~Z portion indicates a pre-release version so a version without it is \"newer\" than a version with it\n# the -W portion has been abandoned but was like the ~Z for large builds and is IGNORED !!!!\n#\tlarge builds now have the same version number as the \"normal\" build\n#\n# the version string passed to this function allows for quite a bit of flexibility\n#\tany alpha characters are permitted prior to the first digit\n#\tup to 3 version parts PLUS a prerelease part are permitted\n#\t\teach with up to 4 digits each -- MORE THAN 4 digits is indeterminate\n#\tthat is: v0.0.0d0  up to v9999.9999.9999b9999 and then v9999.9999.9999 as the highest priority\n#\tany non-numeric character can be used to separate main versions\n#\tspecial significance is assigned to single caracter separators between the numeric strings\n#\t\tb or ~ indicates a beta release\n#\t\ta indicates an alpha release\n#\t\td indicates an development release\n# \t\tthese offset the pre-release number so that b/~ has higher numeric value than any a\n#\t\t\tand a has higher value than d separator\n#\n# a blank version or one without at least one number part is considered invalid\n# alpha and beta seperators require at least two number parts\n#\tif only one number part is found the prerelease seperator is IGNORED\n#\n#\treturns the version number or 0 if string does not parse into needed sections\n\ndef VersionToNumber (version):\n\tversion = version.replace (\"large\",\"L\")\n\tnumberParts = re.split (r'\\D+', version)\n\totherParts = re.split (r'\\d+', version)\n\t# discard blank elements\n\t#\tthis can happen if the version string starts with alpha characters (like \"v\")\n\t# \tof if there are no numeric digits in the version string\n\ttry:\n\t\twhile numberParts [0] == \"\":\n\t\t\tnumberParts.pop(0)\n\texcept:\n\t\tpass\n\n\tnumberPartsLength = len (numberParts)\n\n\tif numberPartsLength == 0:\n\t\treturn 0\n\tversionNumber = 0\n\treleaseType='release'\n\tif numberPartsLength >= 2:\n\t\tif 'b' in otherParts or '~' in otherParts:\n\t\t\treleaseType = 'beta'\n\t\t\tversionNumber += 60000\n\t\telif 'a' in otherParts:\n\t\t\treleaseType = 'alpha'\n\t\t\tversionNumber += 30000\n\t\telif 'd' in otherParts:\n\t\t\treleaseType = 'develop'\n\n\t# if release all parts contribute to the main version number\n\t#\tand offset is greater than all prerelease versions\n\tif releaseType == 'release':\n\t\tversionNumber += 90000\n\t# if pre-release, last part will be the pre release part\n\t#\tand others part will be part the main version number\n\telse:\n\t\tnumberPartsLength -= 1\n\t\tif numberParts [numberPartsLength] != \"\":\n\t\t\tversionNumber += int (numberParts [numberPartsLength])\n\n\t# include core version number\n\tif numberPartsLength >= 1 and numberParts [0] != \"\":\n\t\tversionNumber += int (numberParts [0]) * 10000000000000\n\tif numberPartsLength >= 2 and numberParts [1] != \"\":\n\t\tversionNumber += int (numberParts [1]) * 1000000000\n\tif numberPartsLength >= 3 and numberParts [2] != \"\":\n\t\tversionNumber += int (numberParts [2]) * 100000\n\n\treturn versionNumber\n\n\n# get venus version\nversionFile = \"/opt/victronenergy/version\"\ntry:\n\tfile = open (versionFile, 'r')\nexcept:\n\tVenusVersion = \"\"\n\tVenusVersionNumber = 0\nelse:\n\tVenusVersion = file.readline().strip()\n\tVenusVersionNumber = VersionToNumber (VenusVersion)\n\tfile.close()\n\n#\tPushAction\n#\n# some actions are pushed to one of three queues:\n#\tInstallPackages.InstallQueue for install, uninstall, check and resolveConflicts actions\n#\tDownload.Download for download actions\n# \tAddRemoveQueue for add and remove actions\n#\tGitHubVersion for gitHubScan (GitHub version refresh requiests)\n#\n# other actions are handled in line since they just set a global flag\n#\t\t(not pused on any queue)\n#\t\twhich is then handled elsewere\n#\n# commands are added to the queue from the GUI (dbus service change handler)\n#\tand from the main loop (source = 'AUTO')\n# the queue isolates command triggers from processing because processing \n#\t\tcan take seconds or minutes\n#\n# command is a string: action:packageName\n#\n#\taction is a text string: Install, Uninstall, Download, Add, Remove, etc\n#\tpackageName is the name of the package to receive the action\n#\t\tfor some actions this may be the null string\n#\n# the command and source are pushed on the queue as a tuple\n#\n# PushAction sets the ...Pending flag to prevent duplicate operations\n#\tfor a given package\n#\n# returns True if command was accepted, False if not\n\ndef PushAction (command=None, source=None):\n\tparts = command.split (\":\")\n\ttheQueue = None\n\tif len (parts) >= 1:\n\t\taction = parts[0]\n\telse:\n\t\taction = \"\"\n\tif len (parts) >= 2:\n\t\tpackageName = parts[1]\n\telse:\n\t\tpackageName = \"\"\n\n\tif action == 'download':\n\t\tDbusIf.LOCK (\"PushAction 1\")\n\t\tpackage = PackageClass.LocatePackage (packageName)\n\t\tif package != None:\n\t\t\tpackage.DownloadPending = True\n\t\t\ttheQueue = DownloadGitHub.DownloadQueue\n\t\t\tqueueText = \"Download\"\n\t\t\t# clear the install failure because package contents are changing\n\t\t\t#\tthis allows an auto install again\n\t\t\t#\tbut will be disableed if that install fails\n\t\t\tif source == 'GUI':\n\t\t\t\tDbusIf.UpdateStatus ( message=action  + \" pending \" + packageName, where='Editor' )\n\t\telse:\n\t\t\ttheQueue = None\n\t\t\tqueueText = \"\"\n\t\t\terrorMessage = \"PushAction Download: \" + packageName + \" not in package list\"\n\t\t\tlogging.error (errorMessage)\n\t\t\tif source == 'GUI':\n\t\t\t\tDbusIf.UpdateStatus ( message=errorMessage, where='Editor' )\n\t\t\t\tDbusIf.AcknowledgeGuiEditAction ( 'ERROR', defer=True )\n\t\tDbusIf.UNLOCK (\"PushAction 1\")\n\n\telif action == 'install' or action == 'uninstall' or action == 'check':\n\t\tDbusIf.LOCK (\"PushAction 2\")\n\t\tpackage = PackageClass.LocatePackage (packageName)\n\t\t# flag SetupHelper uninstall for later\n\t\tif packageName == \"SetupHelper\" and action == 'uninstall':\n\t\t\tglobal SetupHelperUninstall\n\t\t\tSetupHelperUninstall = True\n\n\t\tif package != None:\n\t\t\tpackage.InstallPending = True\n\t\t\ttheQueue = InstallPackages.InstallQueue\n\t\t\tqueueText = \"Install\"\n\t\t\tif source == 'GUI':\n\t\t\t\tDbusIf.UpdateStatus ( message=action  + \" pending \" + packageName, where='Editor' )\n\t\telse:\n\t\t\ttheQueue = None\n\t\t\tqueueText = \"\"\n\t\t\terrorMessage = \"PushAction Install: \" + packageName + \" not in package list\"\n\t\t\tlogging.error (errorMessage)\n\t\t\tif source == 'GUI':\n\t\t\t\tDbusIf.UpdateStatus ( message=errorMessage, where='Editor' )\n\t\t\t\tDbusIf.AcknowledgeGuiEditAction ( 'ERROR', defer=True )\n\t\tDbusIf.UNLOCK (\"PushAction 2\")\n\n\telif action == 'resolveConflicts':\n\t\ttheQueue = InstallPackages.InstallQueue\n\t\tqueueText = \"Install\"\n\t\tif source == 'GUI':\n\t\t\t# note this message will be overwritten by the install and uninstall actions\n\t\t\t#\ttriggered by this action\n\t\t\tDbusIf.UpdateStatus ( \"resolving conflicts for \" + packageName, where='Editor' )\n\n\telif action == 'add' or action == 'remove':\n\t\ttheQueue = AddRemove.AddRemoveQueue\n\t\tqueueText = \"AddRemove\"\n\t\tif source == 'GUI':\n\t\t\tDbusIf.UpdateStatus ( message=action  + \" pending \" + packageName, where='Editor' )\n\n\telif action == 'gitHubScan':\n\t\ttheQueue = UpdateGitHubVersion.GitHubVersionQueue\n\t\tqueueText = \"GitHubVersion\"\n\n\t# the remaining actions are handled here (not pushed on a queue)\n\telif action == 'reboot':\n\t\tglobal SystemReboot\n\t\tSystemReboot = True\n\t\tlogging.info ( \"received Reboot request from \" + source)\n\t\tif source == 'GUI':\n\t\t\tDbusIf.UpdateStatus ( message=action  + \" pending \" + packageName, where='Editor' )\n\t\t# set the flag - reboot is done in main_loop\n\t\treturn True\n\telif action == 'restartGui':\n\t\t# set the flag - reboot is done in main_loop\n\t\tglobal GuiRestart\n\t\tGuiRestart = True\n\t\tlogging.info ( \"received GUI restart request from \" + source)\n\t\tif source == 'GUI':\n\t\t\tDbusIf.UpdateStatus ( \"GUI restart pending \" + packageName, where='Editor' )\n\t\treturn True\n\telif action == 'INITIALIZE_PM':\n\t\t# set the flag - Initialize will quit the main loop, then work is done in main\n\t\tglobal InitializePackageManager\n\t\tInitializePackageManager = True\n\t\tlogging.info ( \"received PackageManager INITIALIZE request from \" + source)\n\t\tif source == 'GUI':\n\t\t\tDbusIf.UpdateStatus ( \"PackageManager INITIALIZE pending \" + packageName, where='Editor' )\n\t\treturn True\n\telif action == 'RESTART_PM':\n\t\t# set the flag - Initialize will quit the main loop, then work is done in main\n\t\tglobal RestartPackageManager\n\t\tRestartPackageManager = True\n\t\tlogging.info ( \"received PackageManager RESTART request from \" + source)\n\t\tif source == 'GUI':\n\t\t\tDbusIf.UpdateStatus ( \"PackageManager restart pending \" + packageName, where='Editor' )\n\t\treturn True\n\n\telse:\n\t\tif source == 'GUI':\n\t\t\tDbusIf.UpdateStatus ( message=\"unrecognized command '\" + command + \"'\", where='Editor' )\n\t\t\tDbusIf.AcknowledgeGuiEditAction ( 'ERROR', defer=True )\n\t\tlogging.error (\"PushAction received unrecognized command from \" + source + \": \" + command)\n\t\treturn False\n\n\tif theQueue != None:\n\t\ttry:\n\t\t\ttheQueue.put ( (command, source), block=False )\n\t\t\treturn True\n\t\texcept queue.Full:\n\t\t\tlogging.error (\"command \" + command + \" from \" + source + \" lost - \" + queueText + \" - queue full\")\n\t\t\treturn False\n\t\texcept:\n\t\t\tlogging.error (\"command \" + command + \" from \" + source + \" lost - \" + queueText + \" - other queue error\")\n\t\t\treturn False\n\telse:\n\t\treturn False\n# end PushAction\n\n\n#\tLocatePackagePath\n#\n# attempt to locate a package directory\n#\n# all directories at the current level are checked\n#\tto see if they contain a file named 'version'\n#\tindicating a package directory has been found\n#\tif so, that path is returned\n#\n# if a directory NOT containing 'version' is found\n#\tthis method is called again to look inside that directory\n#\n# if nothing is found, the method returns None\n#\n# all recursive calls will return with the located package or None\n#\tso the original caller will have the path to the package or None\n\ndef LocatePackagePath (origPath):\n\tpaths = os.listdir (origPath)\n\tfor path in paths:\n\t\tnewPath = origPath +'/' + path\n\t\tif os.path.isdir(newPath):\n\t\t\t# found version file, make sure it is \"valid\"\n\t\t\tversionFile = newPath + \"/version\"\n\t\t\tif os.path.isfile( versionFile ):\n\t\t\t\treturn newPath\n\t\t\telse:\n\t\t\t\tpackageDir = locatePackagePath (newPath)\n\t\t\t\t# found a package directory\n\t\t\t\tif packageDir != None:\n\t\t\t\t\treturn packageDir\n\t\t\t\t# nothing found - continue looking in this directory\n\t\t\t\telse:\n\t\t\t\t\tcontinue\n\treturn None\n\n\n#\tAddRemoveClass\n#\tInstances:\n#\t\tAddRemove (a separate thread)\n#\tMethods:\n#\t\trun ( the thread, pulls from  AddRemoveQueue)\n#\t\tStopThread ()\n#\n# some actions called may take seconds or minutes (based on internet speed) !!!!\n#\n# the queue entries are: (\"action\":\"packageName\")\n#\tthis decouples the action from the current package list which could be changing\n#\tallowing the operation to proceed without locking the list\n\nclass AddRemoveClass (threading.Thread):\n\n\tdef __init__(self):\n\t\tthreading.Thread.__init__(self)\n\t\tself.AddRemoveQueue = queue.Queue (maxsize = 50)\n\t\tself.threadRunning = True\n\t\t\n\n\t\n\t#\tAddRemove run (the thread), StopThread\n\t#\n\t# run  is a thread that pulls actions from a queue and processes them\n\t# Note: some processing times can be several seconds to a minute or more\n\t#\tdue to newtork activity\n\t#\n\t# run () checks the threadRunning flag and returns if it is False,\n\t#\tessentially taking the thread off-line\n\t#\tthe main method should catch the tread with join ()\n\t#\n\t# run () also serves as and idle loop to add packages found in /data (AddStoredPacakges)\n\t#\tthis is only called every 3 seconds\n\t#\tand may push add commands onto the AddRemoveQueue\n\t#\n\t# StopThread () is called to shut down the thread\n\n\tdef StopThread (self):\n\t\tself.threadRunning = False\n\t\tself.AddRemoveQueue.put ( ('STOP', ''), block=False )\n\n\t#\tAddRemove run ()\n\t#\n\t# process package Add/Remove actions\n\tdef run (self):\n\t\tglobal RestartPackageManager\n\n\t\tchanges = False\n\t\twhile self.threadRunning:\n\t\t\t# if package was added or removed, don't wait for queue empty\n\t\t\t# so package lists can be updated immediately\n\t\t\tif changes:\n\t\t\t\tdelay = 0.0\n\t\t\telse:\n\t\t\t\tdelay = 3.0\n\t\t\ttry:\n\t\t\t\tcommand = self.AddRemoveQueue.get (timeout = delay)\n\t\t\texcept queue.Empty:\n\t\t\t\t# adds/removes since last queue empty\n\t\t\t\tif changes:\n\t\t\t\t\tDbusIf.UpdateDefaultPackages ()\n\t\t\t\t# no changes so do idle processing:\n\t\t\t\t#\tadd packages in /data that aren't included in package list\n\t\t\t\telse:\n\t\t\t\t\t# restart package manager if a duplice name found in PackageList\n\t\t\t\t\t#\tor if name is not valid\n\t\t\t\t\tDbusIf.LOCK (\"AddRemove run\")\n\t\t\t\t\texistingPackages = []\n\t\t\t\t\tduplicateFound = False\n\t\t\t\t\tfor (index, package) in enumerate (PackageClass.PackageList):\n\t\t\t\t\t\tpackageName = package.PackageName\n\t\t\t\t\t\tif packageName in existingPackages or not PackageClass.PackageNameValid (packageName):\n\t\t\t\t\t\t\tduplicateFound = True\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\texistingPackages.append (packageName)\n\t\t\t\t\tdel existingPackages\n\t\t\t\t\tDbusIf.UNLOCK (\"AddRemove run\")\n\t\t\t\t\t# exit this thread so no more package adds/removes are possible\n\t\t\t\t\t#\tPackageManager will eventually reset\n\t\t\t\t\tif duplicateFound:\n\t\t\t\t\t\tlogging.critical (\"duplicate \" + packageName + \" found in package list - restarting PackageManager\")\n\t\t\t\t\t\tRestartPackageManager = True\n\t\t\t\t\t\treturn\n\n\t\t\t\t\tPackageClass.AddStoredPackages ()\n\n\t\t\t\tchanges = False\n\t\t\t\tcontinue\n\t\t\texcept:\n\t\t\t\tlogging.error (\"pull from AddRemoveQueue failed\")\n\t\t\t\tcontinue\n\t\t\tif len (command) == 0:\n\t\t\t\tlogging.error (\"pull from AddRemove queue failed - empty comand\")\n\t\t\t\tcontinue\n\t\t\t# thread shutting down\n\t\t\tif command [0] == 'STOP' or self.threadRunning == False:\n\t\t\t\treturn\n\n\t\t\t# separate command, source tuple\n\t\t\t# and separate action and packageName\n\t\t\tif len (command) >= 2:\n\t\t\t\tparts = command[0].split (\":\")\n\t\t\t\tif len (parts) >= 2:\n\t\t\t\t\taction = parts[0].strip ()\n\t\t\t\t\tpackageName = parts[1].strip ()\n\t\t\t\telse:\n\t\t\t\t\tlogging.error (\"AddRemoveQueue - no action or no package name - discarding\", command)\n\t\t\t\t\tcontinue\n\t\t\t\tsource = command[1]\n\t\t\telse:\n\t\t\t\tlogging.error (\"AddRemoveQueue - no command and/or source - discarding\", command)\n\t\t\t\tcontinue\n\n\t\t\tif action == 'add':\n\t\t\t\tpackageDir = \"/data/\" + packageName\n\t\t\t\tif source == 'GUI':\n\t\t\t\t\tuser = DbusIf.EditPackage.GitHubUser\n\t\t\t\t\tbranch = DbusIf.EditPackage.GitHubBranch\n\t\t\t\telse:\n\t\t\t\t\tuser = \"\"\n\t\t\t\t\tbranch = \"\"\n\t\t\t\t# try to get GitHub info from package directory\n\t\t\t\tif user == \"\":\n\t\t\t\t\tif os.path.isdir (packageDir):\n\t\t\t\t\t\tgitHubInfoFile = packageDir + \"/gitHubInfo\"\n\t\t\t\t\t\ttry:\n\t\t\t\t\t\t\tfd = open (gitHubInfoFile, 'r')\n\t\t\t\t\t\t\tparts = fd.readline().strip ().split (':')\n\t\t\t\t\t\t\tfd.close()\n\t\t\t\t\t\texcept:\n\t\t\t\t\t\t\tparts = \"\"\n\t\t\t\t\t\tif len (parts) >= 2:\n\t\t\t\t\t\t\tuser = parts[0]\n\t\t\t\t\t\t\tbranch = parts[1]\n\t\t\t\t# still nothing - try to get GitHub info from default package list\n\t\t\t\tif user == \"\":\n\t\t\t\t\tdefault = DbusIf.LocateRawDefaultPackage (packageName)\n\t\t\t\t\tif default != None:\n\t\t\t\t\t\tuser = default[1]\n\t\t\t\t\t\tbranch = default[2]\n\n\t\t\t\tif PackageClass.AddPackage (packageName = packageName, source=source,\n\t\t\t\t\t\t\t\tgitHubUser=user, gitHubBranch=branch ):\n\t\t\t\t\tchanges = True\n\n\t\t\telif action == 'remove':\n\t\t\t\tif PackageClass.RemovePackage ( packageName=packageName ):\n\t\t\t\t\tchanges = True\n\t\t\telse:\n\t\t\t\tlogging.warning ( \"received invalid action \" + command + \" from \" + source + \" - discarding\" )\n\t\t# end while True\n\t# end run ()\n# end AddRemoveClass\n\n\n#\tDbusIfClass\n#\tInstances:\n#\t\tDbusIf\n#\tMethods:\n#\t\tRemoveDbusSettings (class method)\n#\t\tUpdateStatus\n#\t\tvarious Gets and Sets for dbus parameters\n#\t\thandleGuiEditAction (dbus change handler)\n#\t\tLocateRawDefaultPackage\n#\t\tUpdateDefaultPackages ()\n#\t\tReadDefaultPackagelist ()\n#\t\tLOCK ()\n#\t\tUNLOCK ()\n#\t\tRemoveDbusService ()\n#\n#\tGlobals:\n#\t\tDbusSettings (for settings that are NOT part of a package)\n#\t\tDbusService (for parameters that are NOT part of a package)\n#\t\tEditPackage - the dbus Settings used by the GUI to hand off information about\n#\t\t\ta new package\n#\t\tDefaultPackages - list of default packages, each a tuple:\n#\t\t\t\t\t\t ( packageName, gitHubUser, gitHubBranch)\n#\n# DbusIf manages the dbus Settings and packageManager dbus service parameters\n#\tthat are not associated with any spcific package\n#\n#\tthe dbus settings managed here do NOT have a package association\n#\thowever, the per-package parameters from PackageClass are ADDED to\n#\tDbusSettings and dBusService created here !!!!\n#\n# DbusIf manages a lock to prevent data access in one thread\n#\twhile it is being changed in another\n#\tthe same lock is used to protect data in PackageClass also\n#\tthis is more global than it needs to be but simplies the locking\n#\n#\tall methods that access must aquire this lock\n#\t\tprior to accessing DbusIf or Package data\n#\t\tthen must release the lock\n#\t\tFALURE TO RELEASE THE LOCK WILL HANG OTHER THREADS !!!!!\n#\n# default package info is fetched from a file and published to our dbus service\n#\tfor use by the GUI in adding new packages\n#\tthe default info is also stored in defaultPackageList []\n#\tLocateRawDefaultPackage is used to retrieve the default from local storage\n#\t\trather than pulling from dbus or reading the file again to save time\n\nclass DbusIfClass:\n\n\t#\t\tRemoveDbusSettings\n\t# remove the dbus Settings paths for package\n\t# package Settings are removed\n\t# this is called when removing a package\n\t# settings to be removed are passed as a list (settingsList)\n\t# this gets reformatted for the call to dbus\n\n\t@classmethod\n\tdef RemoveDbusSettings (cls, settingsList):\n\n\t\t# format the list of settings to be removed\n\t\ti = 0\n\t\twhile i < len (settingsList):\n\t\t\tif i == 0:\n\t\t\t\tsettingsToRemove = '%[ \"' + settingsList[i]\n\t\t\telse:\n\t\t\t\tsettingsToRemove += '\" , \"' + settingsList[i]\n\t\t\ti += 1\n\t\tsettingsToRemove += '\" ]'\n\n\t\t# remove the dbus Settings paths - via the command line \n\t\ttry:\n\t\t\tproc = subprocess.Popen (['dbus', '-y', 'com.victronenergy.settings', '/',\n\t\t\t\t\t\t'RemoveSettings', settingsToRemove  ],\n\t\t\t\t\t\tbufsize=-1, stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n\t\t\t_, stderr = proc.communicate ()\n\t\t\tstderr = stderr.decode ().strip ()\n\t\t\treturnCode = proc.returncode\n\t\texcept:\n\t\t\tlogging.error (\"dbus RemoveSettings call failed\")\n\t\telse:\n\t\t\tif returnCode != 0:\n\t\t\t\tlogging.error (\"dbus RemoveSettings failed \" + str (returnCode))\n\t\t\t\tlogging.error (\"stderr: \" + stderr)\n\t\n\t#\tUpdateStatus\n\t#\n\t# updates the status when the operation completes\n\t# the GUI provides three different areas to show status\n\t# where specifies which of these are updated\n\t#\t'PmStatus'\n\t#\t'Editor'\n\t#\t'Media'\n\t#\twhich determines where status is sent\n\t# message is the text displayed\n\t# if LogLevel is not 0, message is also written to the PackageManager log\n\t# logging levels: (can use numeric value or these variables set at head of module\n\t#\tCRITICAL = 50\n\t#\tERROR = 40\n\t#\tWARNING = 30\n\t#\tINFO = 20\n\t#\tDEBUG = 10\n\t# if where = None, no GUI status areas are updated\n\n\n\tdef UpdateStatus ( self, message=None, where=None, logLevel=0 ):\n\n\t\tif logLevel != 0:\n\t\t\tlogging.log ( logLevel, message )\n\n\t\tif where == 'Editor':\n\t\t\tDbusIf.SetEditStatus ( message )\n\t\telif where == 'PmStatus':\n\t\t\tDbusIf.SetPmStatus ( message )\n\t\telif where == 'Media':\n\t\t\tDbusIf.SetMediaStatus (message)\n\n\tdef UpdatePackageCount (self):\n\t\tcount = len(PackageClass.PackageList)\n\t\tself.DbusSettings['packageCount'] = count\n\tdef GetPackageCount (self):\n\t\treturn self.DbusSettings['packageCount']\n\tdef SetAutoDownloadMode (self, value):\n\t\tself.DbusSettings['autoDownload'] = value\n\tdef GetAutoDownloadMode (self):\n\t\treturn self.DbusSettings['autoDownload']\n\tdef GetAutoInstall (self):\n\t\treturn self.DbusSettings['autoInstall'] == 1\n\tdef SetAutoInstall (self, value):\n\t\tif value == True:\n\t\t\tdbusValue = 1\n\t\telse:\n\t\t\tdbusValue = 0\n\t\tself.DbusSettings['autoInstall'] = dbusValue\n\tdef SetPmStatus (self, value):\n\t\tself.DbusService['/PmStatus'] = value\n\tdef SetMediaStatus (self, value):\n\t\tself.DbusService['/MediaUpdateStatus'] = value\n\n\tdef SetDefaultCount (self, value):\n\t\tself.DbusService['/DefaultCount'] = value\n\tdef GetDefaultCount (self):\n\t\treturn self.DbusService['/DefaultCount']\n\n\tdef SetBackupMediaAvailable (self, value):\n\t\tif value == True:\n\t\t\tdbusValue = 1\n\t\telse:\n\t\t\tdbusValue = 0\n\t\tself.DbusService['/BackupMediaAvailable'] = dbusValue\n\tdef GetBackupMediaAvailable (self):\n\t\tif self.DbusService['/BackupMediaAvailable'] == 1:\n\t\t\treturn True\n\t\telse:\n\t\t\treturn True\n\n\tdef SetBackupSettingsFileExist (self, value):\n\t\tif value == True:\n\t\t\tdbusValue = 1\n\t\telse:\n\t\t\tdbusValue = 0\n\t\tself.DbusService['/BackupSettingsFileExist'] = dbusValue\n\n\tdef SetBackupSettingsLocalFileExist (self, value):\n\t\tif value == True:\n\t\t\tdbusValue = 1\n\t\telse:\n\t\t\tdbusValue = 0\n\t\tself.DbusService['/BackupSettingsLocalFileExist'] = dbusValue\n\n\tdef GetBackupSettingsFileExist (self):\n\t\tif self.DbusService['/BackupSettingsFileExist'] == 1:\n\t\t\treturn True\n\t\telse:\n\t\t\treturn True\n\n\tdef SetBackupProgress (self, value):\n\t\tself.DbusService['/BackupProgress'] = value\n\tdef GetBackupProgress (self):\n\t\treturn self.DbusService['/BackupProgress']\n\n\n\t#\tAcknowledgeGuiEditAction\n\t# is part of the PackageManager to GUI communication\n\t# the GUI set's an action triggering some processing here\n\t# \tvia the dbus change handler\n\t# PM updates this dbus value when processing completes \n\t#\tsignaling either success or failure\n\t#\n\t# acknowledgements can not be sent from within the GuiEditAction handler\n\t#\tso these are deferred and processed in mainLoop\n\t\n\tdef AcknowledgeGuiEditAction (self, value, defer=False):\n\t\tglobal DeferredGuiEditAcknowledgement\n\n\t\tif defer:\n\t\t\tDeferredGuiEditAcknowledgement = value\n\t\telse:\n\t\t\t# delay acknowledgement slightly to prevent lockups in the dbus system\n\t\t\ttime.sleep (0.002)\n\t\t\tself.DbusService['/GuiEditAction'] = value\n\n\tdef SetEditStatus (self, message):\n\t\tself.DbusService['/GuiEditStatus'] = message\n\n\n\n\t#\thandleGuiEditAction (internal use only)\n\t#\n\t# the GUI uses PackageManager service /GuiEditAction\n\t# to request the PackageManager perform some action\n\t#\n\t# this handler disposes of the request quickly by pushing\n\t#\tthe command onto a queue or setting a global variable for later processing\n\n\t# errors that occur from the handler thread can not set the GuiEditAction\n\t#\tsince the parameter will be set to the value passed to the handler\n\t# \toverriding the one set within the handler thread\n\n\tdef handleGuiEditAction (self, path, command):\n\t\tglobal PushAction\n\t\t# ignore a blank command - this happens when the command is cleared\n\t\t#\tand should not trigger further action\n\t\tif command == \"\":\n\t\t\tpass\n\t\telse:\n\t\t\tPushAction ( command=command, source='GUI' )\n\t\treturn True\t# True acknowledges the dbus change - otherwise dbus parameter does not change\n\n\n\t# search RAW default package list for packageName\n\t# and return the pointer if found\n\t#\totherwise return None\n\t#\n\t# Note: the raw default package list is built during init\n\t#\tthen never changes so LOCK/UNLOCK is NOT needed\n\t#\n\t# rawDefaultPackages is a list of tuples:\n\t#\t(packageName, gitHubUser, gitHubBranch)\n\t#\n\t# if a packageName match is found, the tuple is returned\n\t#\totherwise None is retuned\n\n\tdef LocateRawDefaultPackage (self, packageName):\n\t\t\n\t\tfor default in self.rawDefaultPackages:\n\t\t\tif packageName == default[0]:\n\t\t\t\treturn default\n\t\treturn None\n\n\n\t#\tUpdateDefaultPackages\n\t#\n\t# refreshes the defaultPackageList to include only packages NOT be in PackageList\n\t# this also updates the dbus default packages used by the GUI Add Package menu\n\n\tdef UpdateDefaultPackages (self):\n\t\tDbusIf.LOCK (\"UpdateDefaultPackages\")\n\t\t# don't touch \"new\" entry (index 0)\n\t\tindex = 1\n\t\toldDefaultCount = len (self.defaultPackageList)\n\t\tfor default in self.rawDefaultPackages:\n\t\t\t# if not in the main package list, add it to the default package list\n\t\t\tname = default[0]\n\t\t\tif PackageClass.LocatePackage (name) == None:\n\t\t\t\tuser = default[1]\n\t\t\t\tbranch = default[2]\n\t\t\t\tprefix = '/Default/' + str (index) + '/'\n\t\t\t\t# this entry already exists - update it\n\t\t\t\tif index < oldDefaultCount:\n\t\t\t\t\t# name has changed, update the entry (local and dbus)\n\t\t\t\t\tif (name != self.defaultPackageList[index][0]):\n\t\t\t\t\t\tself.defaultPackageList[index] = default\n\t\t\t\t\t\tself.DbusService[prefix + 'PackageName'] = name\n\t\t\t\t\t\tself.DbusService[prefix + 'GitHubUser'] = user\n\t\t\t\t\t\tself.DbusService[prefix + 'GitHubBranch'] = branch\n\t\t\t\t# path doesn't yet exist, add it\t\n\t\t\t\telse:\n\t\t\t\t\tself.defaultPackageList.append (default)\n\t\t\t\t\tself.DbusService.add_path (prefix + 'PackageName', name )\n\t\t\t\t\tself.DbusService.add_path (prefix + 'GitHubUser', user )\n\t\t\t\t\tself.DbusService.add_path (prefix + 'GitHubBranch', branch )\n\n\t\t\t\tindex += 1\n\n\t\tself.DbusService['/DefaultCount'] = index \n\n\t\t# clear out any remaining path values\n\t\twhile index < oldDefaultCount:\n\t\t\tprefix = '/Default/' + str (index) + '/'\n\t\t\tself.defaultPackageList[index] = ( \"\", \"\", \"\" )\n\t\t\tself.DbusService[prefix + 'PackageName'] = \"\"\n\t\t\tself.DbusService[prefix + 'GitHubUser'] = \"\"\n\t\t\tself.DbusService[prefix + 'GitHubBranch'] = \"\"\n\t\t\tindex += 1\n\n\t\tDbusIf.UNLOCK (\"UpdateDefaultPackages\")\n\n\n\t#\tReadDefaultPackagelist\n\t#\n\t# read in the default packages list file and store info locally for faster access later\n\t# this list is only used to populate the defaultPackageList which excludes packages that\n\t#\tare in the main Packagelist\n\n\tdef ReadDefaultPackagelist (self):\n\n\t\ttry:\n\t\t\tlistFile = open (\"/data/SetupHelper/defaultPackageList\", 'r')\n\t\texcept:\n\t\t\tlogging.error (\"no defaultPackageList \" + listFileName)\n\t\telse:\n\t\t\tfor line in listFile:\n\t\t\t\tparts = line.split ()\n\t\t\t\tif len(parts) < 3 or line[0] == \"#\":\n\t\t\t\t\tcontinue\n\t\t\t\tself.rawDefaultPackages.append ( ( parts[0], parts[1], parts[2] ) )\n\t\t\tlistFile.close ()\n\n\n\t# LOCK and UNLOCK - capitals used to make it easier to identify in the code\n\t#\n\t# these protect the package list from changing while the list is being accessed\n\t#\n\t# locked sections of code should execute quickly to minimize impact on other threads\n\t#\n\t# failure to UNLOCK will result in a LOCK request from another thread timing out\n\t#\n\t# lock requests that time out result in PackageManager exiting immediately without cleanup\n\t# supervise will then restart it\n\t\n\tdef LOCK (self, name):\n\t\trequestTime = time.time()\n\t\treportTime = requestTime\n\t\twhile True:\n\t\t\tif self.lock.acquire (blocking=False):\n\t\t\t\t# here if lock was acquired\n\t\t\t\treturn\n\t\t\telse:\n\t\t\t\ttime.sleep (0.1)\n\t\t\t\tcurrentTime = time.time()\n\t\t\t\t# waiting for 5 seconds - timeout\n\t\t\t\tif currentTime - requestTime > 5.0:\n\t\t\t\t\tlogging.critical (\"timeout waiting for lock \" + name + \" - restarting PackageManager\")\n\t\t\t\t\tos._exit(1)\t\t\n\t\t\t\t# report waiting every 1 second\n\t\t\t\telif currentTime - reportTime > 0.5:\n\t\t\t\t\tlogging.warning (\"waiting to aquire lock \" + name)\n\t\t\t\t\treportTime = currentTime\n\n\t\t\n\tdef UNLOCK (self, name):\n\t\ttry:\n\t\t\tself.lock.release ()\n\t\texcept RuntimeError:\n\t\t\tlogging.error (\"UNLOCK when not locked - continuing \" + name)\n\t\t\t\n\n\tdef __init__(self):\n\t\tself.lock = threading.RLock()\n\t\tsettingsList = {'packageCount': [ '/Settings/PackageManager/Count', 0, 0, 0 ],\n\t\t\t\t\t\t'autoDownload': [ '/Settings/PackageManager/GitHubAutoDownload', 0, 0, 0 ],\n\t\t\t\t\t\t'autoInstall': [ '/Settings/PackageManager/AutoInstall', 0, 0, 0 ],\n\t\t\t\t\t\t}\n\t\tself.DbusSettings = SettingsDevice(bus=dbus.SystemBus(), supportedSettings=settingsList,\n\t\t\t\t\t\t\t\ttimeout = 30, eventCallback=None )\n\n\t\t# check firmware version and delay dbus service registration for v3.40~38 and beyond\n\t\tglobal VenusVersionNumber\n\t\tglobal VersionToNumber\n\t\tself.DbusService = VeDbusService ('com.victronenergy.packageManager', bus = dbus.SystemBus(), register=False)\n\t\t\n\t\tself.DbusService.add_mandatory_paths (\n\t\t\t\t\t\t\tprocessname = 'PackageManager', processversion = 1.0, connection = 'none',\n\t\t\t\t\t\t\tdeviceinstance = 0, productid = 1, productname = 'Package Manager',\n\t\t\t\t\t\t\tfirmwareversion = 1, hardwareversion = 0, connected = 1)\n\t\tself.DbusService.add_path ( '/MediaUpdateStatus', \"\", writeable = True )\n\t\tself.DbusService.add_path ( '/GuiEditStatus', \"\", writeable = True )\n\n\t\tself.DbusService.add_path ( '/GuiEditAction', \"\", writeable = True,\n\t\t\t\t\t\t\t\t\t\tonchangecallback = self.handleGuiEditAction )\n\n\t\t# initialize default package list to empty - entries will be added later\n\t\tself.DbusService.add_path ('/DefaultCount', 0 )\n\n\t\t# a special package used for editing a package prior to adding it to Package list\n\t\tself.EditPackage = PackageClass ( section = \"Edit\", packageName = \"\" )\n\t\t\n\t\tself.rawDefaultPackages = []\n\t\tself.defaultPackageList = []\n\n\t\t# create first default package, place where a new package is entered from scratch\n\t\tself.defaultPackageList.append ( (\"new\", \"\", \"\") )\n\t\tself.DbusService.add_path ( \"/Default/0/PackageName\", \"new\" )\n\t\tself.DbusService.add_path ( \"/Default/0/GitHubUser\", \"\" )\n\t\tself.DbusService.add_path ( \"/Default/0/GitHubBranch\", \"\" )\n\n\t\t# used to notify the GUI that an action is required to complete a manual installation\n\t\t#\tthe operator has the option to defer reboot and GUI restart operations\n\t\t#\tif they do, this parameter is set and a button appears on the main Package manager menu\n\n\t\tself.DbusService.add_path ( \"/ActionNeeded\", '' )\n\n\t\tself.DbusService.add_path ( '/BackupMediaAvailable', 0, writeable = True )\n\t\tself.DbusService.add_path ( '/BackupSettingsFileExist', 0, writeable = True )\n\t\tself.DbusService.add_path ( '/BackupSettingsLocalFileExist', 0, writeable = True )\n\t\tself.DbusService.add_path ( '/BackupProgress', 0, writeable = True )\n\n\t\t# do these last because the GUI uses them to check if PackageManager is running\n\t\tself.DbusService.add_path ( '/PmStatus', \"\", writeable = True )\n\t\tglobal Platform\n\t\tself.DbusService.add_path ( '/Platform', Platform )\n\n\t\tself.DbusService.register ()\n\n\n\t#\tRemoveDbusService\n\t#  deletes the dbus service\n\n\tdef RemoveDbusService (self):\n\t\tlogging.info (\"shutting down com.victronenergy.packageManager dbus service\")\n\t\tself.DbusService.__del__()\n\t\n# end DbusIf\n\n#\tPackageClass\n#\tInstances:\n#\t\tone per package\n#\n#\tMethods:\n#\t\tLocatePackage\n#\t\tGetAutoAddOk (class method)\n#\t\tSetAutoAddOk (class method)\n#\t\tSetAutoInstallOk ()\n#\t\tsettingChangedHandler ()\n#\t\tvarious Gets and Sets\n#\t\tAddPackagesFromDbus (class method)\n#\t\tPackageNameValid (class method)\n#\t\tAddStoredPackages (class method)\n#\t\tAddPackage (class method)\n#\t\tRemovePackage (class method)\n#\t\tUpdateVersionsAndFlags ()\n#\n#\tGlobals:\n#\t\tPackageList [] - list instances of all packages\n#\t\tDbusSettings (for per-package settings)\n#\t\tDbusService (for per-package parameters)\n#\t\tDownloadPending\n#\t\tInstallPending\n#\n# a package consits of Settings and version parameters in the PackageMonitor dbus service\n# all Settings and parameters are accessible via set... and get... methods\n#\tso that the caller does not need to understand dbus Settings and service syntax\n# the packageName variable maintains a local copy of the dBus parameter for speed in loops\n# section passed to init can be either a int or string ('Edit')\n#\tan int is converted to a string to form the dbus setting paths\n#\n# the dbus settings and service parameters managed here are on a per-package basis\n\nclass PackageClass:\n\n\t# list of instantiated Packages\n\tPackageList = []\n\n\t# search PackageList for packageName\n\t# and return the package pointer if found\n\t#\totherwise return None\n\t#\n\t# Note: this method should be called with LOCK () set\n\t#\tand use the returned value before UNLOCK ()\n\t#\tto avoid unpredictable results\n\n\t@classmethod\n\tdef LocatePackage (cls, packageName):\n\t\tfor package in PackageClass.PackageList:\n\t\t\tif packageName == package.PackageName:\n\t\t\t\treturn package\n\t\treturn None\n\n\n\t# this set of methods manages the flag files that control\n\t#\tautomaticly adding and installing packages\n\t# if a package is manually removed, it should not\n\t#\tbe readded automatically\n\t# ditto for manual uninstall\n\t#\n\n\t@classmethod\n\tdef GetAutoAddOk (cls, packageName):\n\t\tif packageName == None:\n\t\t\tlogging.error (\"GetAutoAddOk - no packageName\")\n\t\t\treturn False\n\n\t\tflagFile = \"/data/setupOptions/\" + packageName + \"/DO_NOT_AUTO_ADD\"\n\t\tif os.path.exists (flagFile):\n\t\t\treturn False\n\t\telse:\n\t\t\treturn True\n\n\n\t@classmethod\n\tdef SetAutoAddOk (cls, packageName, state):\n\t\tif packageName == None:\n\t\t\tlogging.error (\"SetAutoAddOk - no packageName\")\n\t\t\treturn\n\n\t\t# if package options directory exists set/clear auto add flag\n\t\t# directory may not exist if package was never downloaded or transferred from media\n\t\t#\tor if package was added manually then never acted on\n\t\toptionsDir = \"/data/setupOptions/\" + packageName\n\t\tif os.path.exists (optionsDir):\n\t\t\tflagFile = optionsDir + \"/DO_NOT_AUTO_ADD\"\n\t\t\t# permit auto add\n\t\t\tif state == True:\n\t\t\t\tif os.path.exists (flagFile):\n\t\t\t\t\tos.remove (flagFile)\n\t\t\t# block auto add\n\t\t\telse:\n\t\t\t\tif not os.path.exists (flagFile):\n\t\t\t\t\t# equivalent to unix touch command\n\t\t\t\t\topen (flagFile, 'a').close()\n\n\n\tdef SetAutoInstallOk (self, state):\n\t\tpackageName = self.PackageName\n\t\tif packageName == None:\n\t\t\tlogging.error (\"SetAutoInstallOk - no packageName\")\n\t\t\treturn\n\n\t\t# if package options directory exists set/clear auto install flag\n\t\t# directory may not exist if package was never downloaded or transferred from media\n\t\t#\tor if package was added manually then never acted on\n\t\toptionsDir = \"/data/setupOptions/\" + packageName\n\t\tif os.path.exists (optionsDir):\n\t\t\tflagFile = optionsDir + \"/DO_NOT_AUTO_INSTALL\"\n\t\t\t# permit auto installs\n\t\t\tif state == True:\n\t\t\t\tif os.path.exists (flagFile):\n\t\t\t\t\tos.remove (flagFile)\n\t\t\t# block auto install\n\t\t\telse:\n\t\t\t\tif not os.path.exists (flagFile):\n\t\t\t\t\topen (flagFile, 'a').close()\n\n\n\tdef SetPackageName (self, newName):\n\t\tself.DbusSettings['packageName'] = newName\n\t\tself.PackageName = newName\n\n\tdef SetInstalledVersion (self, version):\n\t\tglobal VersionToNumber\n\t\tself.InstalledVersion = version\n\t\tself.InstalledVersionNumber = VersionToNumber (version)\n\t\tif self.installedVersionPath != \"\":\n\t\t\tDbusIf.DbusService[self.installedVersionPath] = version\t\n\n\tdef SetPackageVersion (self, version):\n\t\tglobal VersionToNumber\n\t\tself.PackageVersion = version\n\t\tself.PackageVersionNumber = VersionToNumber (version)\n\t\tif self.packageVersionPath != \"\":\n\t\t\tDbusIf.DbusService[self.packageVersionPath] = version\t\n\n\tdef SetGitHubVersion (self, version):\n\t\tglobal VersionToNumber\n\t\tself.GitHubVersion = version\n\t\tself.GitHubVersionNumber = VersionToNumber (version)\n\t\tif self.gitHubVersionPath != \"\":\n\t\t\tDbusIf.DbusService[self.gitHubVersionPath] = version\n\n\tdef SetGitHubUser (self, user):\n\t\tself.GitHubUser = user\n\t\tself.DbusSettings['gitHubUser'] = user\n\n\tdef SetGitHubBranch (self, branch):\n\t\tself.GitHubBranch = branch\n\t\tself.DbusSettings['gitHubBranch'] = branch\n\n\tdef SetIncompatible (self, value, details=\"\", resolvable=False):\n\t\tself.Incompatible = value\n\t\tself.IncompatibleDetails = details\n\t\tself.IncompatibleResolvable = resolvable\n\t\tif self.incompatiblePath != \"\":\n\t\t\tDbusIf.DbusService[self.incompatiblePath] = value\t\n\t\tif self.incompatibleDetailsPath != \"\":\n\t\t\tDbusIf.DbusService[self.incompatibleDetailsPath] = details\n\t\tif self.IncompatibleResolvablePath != \"\":\n\t\t\tif resolvable:\n\t\t\t\tDbusIf.DbusService[self.IncompatibleResolvablePath] = 1\n\t\t\telse:\n\t\t\t\tDbusIf.DbusService[self.IncompatibleResolvablePath] = 0\n\n\tdef settingChangedHandler (self, name, old, new):\n\t\t# when dbus information changes, need to refresh local mirrors\n\t\tif name == 'packageName':\n\t\t\tself.PackageName = new\n\t\telif name == 'gitHubBranch':\n\t\t\tself.GitHubBranch = new\n\t\t\tif self.PackageName != None and self.PackageName != \"\":\n\t\t\t\tUpdateGitHubVersion.SetPriorityGitHubVersion ( 'package:' + self.PackageName )\n\t\telif name == 'gitHubUser':\n\t\t\tself.GitHubUser = new\n\t\t\tif self.PackageName != None and self.PackageName != \"\":\n\t\t\t\tUpdateGitHubVersion.SetPriorityGitHubVersion ( 'package:' + self.PackageName )\n\n\tdef __init__( self, section, packageName = None ):\n\t\t# add package parameters if it's a real package (not Edit)\n\t\tif section != 'Edit':\n\t\t\tsection = str (section)\n\t\t\tself.gitHubVersionPath = '/Package/' + section + '/GitHubVersion'\n\t\t\tself.packageVersionPath = '/Package/' + section + '/PackageVersion'\n\t\t\tself.installedVersionPath = '/Package/' + section + '/InstalledVersion'\n\t\t\tself.incompatiblePath = '/Package/' + section + '/Incompatible'\n\t\t\tself.incompatibleDetailsPath = '/Package/' + section + '/IncompatibleDetails'\n\t\t\tself.IncompatibleResolvablePath = '/Package/' + section + '/IncompatibleResolvable'\n\n\t\t\t# create service paths if they don't already exist\n\t\t\ttry:\n\t\t\t\tfoo = DbusIf.DbusService[self.installedVersionPath]\n\t\t\texcept:\n\t\t\t\tDbusIf.DbusService.add_path (self.installedVersionPath, \"\" )\n\t\t\ttry:\n\t\t\t\tfoo = DbusIf.DbusService[self.gitHubVersionPath]\n\t\t\texcept:\n\t\t\t\tDbusIf.DbusService.add_path (self.gitHubVersionPath, \"\" )\n\t\t\ttry:\n\t\t\t\tfoo = DbusIf.DbusService[self.packageVersionPath]\n\t\t\texcept:\n\t\t\t\tDbusIf.DbusService.add_path (self.packageVersionPath, \"\" )\n\t\t\ttry:\n\t\t\t\tfoo = DbusIf.DbusService[self.incompatiblePath]\n\t\t\texcept:\n\t\t\t\tDbusIf.DbusService.add_path (self.incompatiblePath, \"\" )\n\t\t\ttry:\n\t\t\t\tfoo = DbusIf.DbusService[self.incompatibleDetailsPath]\n\t\t\texcept:\n\t\t\t\tDbusIf.DbusService.add_path (self.incompatibleDetailsPath, \"\" )\n\t\t\ttry:\n\t\t\t\tfoo = DbusIf.DbusService[self.IncompatibleResolvablePath]\n\t\t\texcept:\n\t\t\t\tDbusIf.DbusService.add_path (self.IncompatibleResolvablePath, \"\" )\n\n\t\tself.packageNamePath = '/Settings/PackageManager/' + section + '/PackageName'\n\t\tself.gitHubUserPath = '/Settings/PackageManager/' + section + '/GitHubUser'\n\t\tself.gitHubBranchPath = '/Settings/PackageManager/' + section + '/GitHubBranch'\n\n\t\t# temporarily set PackageName since settingChangeHandler may be called as soon as SettingsDevice is called\n\t\t#\twhich is before actual package name is set below\n\t\t#\tso this avoids a crash\n\t\tself.PackageName = \"\"\n\n\t\tsettingsList =\t{'packageName': [ self.packageNamePath, '', 0, 0 ],\n\t\t\t\t\t\t'gitHubUser': [ self.gitHubUserPath, '', 0, 0 ],\n\t\t\t\t\t\t'gitHubBranch': [ self.gitHubBranchPath, '', 0, 0 ],\n\t\t\t\t\t\t}\n\t\tself.DbusSettings = SettingsDevice(bus=dbus.SystemBus(), supportedSettings=settingsList,\n\t\t\t\teventCallback=self.settingChangedHandler, timeout = 10)\n\n\t\t# if packageName specified on init, use that name\n\t\tif packageName != None:\n\t\t\tself.DbusSettings['packageName'] = packageName\n\t\t\tself.PackageName = packageName\n\t\t# otherwise pull name from dBus Settings\n\t\t#\t this happens when adding a package when it is already in dbus\n\t\telse:\n\t\t\tself.PackageName = self.DbusSettings['packageName']\n\t\tself.GitHubUser = self.DbusSettings['gitHubUser']\n\t\tself.GitHubBranch = self.DbusSettings['gitHubBranch']\n\t\t\n\t\t# these flags are used to insure multiple actions aren't executed on top of each other\n\t\tself.DownloadPending = False\n\t\tself.InstallPending = False\n\t\tself.InstallAfterDownload = False\t# used by ResolveConflicts when doing both download and install\n\n\t\tself.AutoInstallOk = False\n\t\tself.DependencyErrors = []\n\t\tself.FileConflicts = []\n\t\tself.LastPatchErrorUpdate = 0\n\t\tself.ConflictsResolvable = True\n\n\t\tself.ActionNeeded = ''\n\n\t\tself.lastScriptPrecheck = 0\n\n\t\tself.lastGitHubRefresh = 0\n\n\t\t# init dbus parameters\n\t\t# these have local values also to speed access\n\t\t# only create service parameters for real packages\n\t\tif section != 'Edit':\n\t\t\tself.SetInstalledVersion (\"\")\n\t\t\tself.SetPackageVersion (\"\")\n\t\t\tself.SetGitHubVersion (\"\")\n\t\t\tself.SetIncompatible (\"\")\n\t\t\t# copy dbus info to local values\n\t\t\tself.GitHubUser = self.DbusSettings['gitHubUser']\n\t\t\tself.GitHubBranch = self.DbusSettings['gitHubBranch']\n\t\t# init edit GitHub info\n\t\telse:\n\t\t\tself.SetGitHubUser (\"?\")\n\t\t\tself.SetGitHubBranch (\"?\")\n\n\n\t# dbus Settings is the primary non-volatile storage for packageManager\n\t# upon startup, PackageList [] is empty and we need to populate it\n\t# from previous dBus Settings in /Settings/PackageManager/...\n\t# this is a special case that can't use AddPackage below:\n\t#\twe do not want to create any new Settings !!\n\t#\tit should be \"safe\" to limit the serch to 0 to < packageCount\n\t#\twe also don't specify any parameters other than the section (index)\n\t#\n\t# NOTE: this method is called before threads are created so do not LOCK\n\t#\n\t# returns False if couldn't get the package count from dbus\n\t#\totherwise returns True\n\t# no package count on dbus is an error that would prevent continuing\n\t# this should never happen since the DbusIf is instantiated before this call\n\t#\twhich creates /Count if it does not exist\n\n\t@classmethod\n\tdef AddPackagesFromDbus (cls):\n\t\tglobal DbusIf\n\t\tpackageCount = DbusIf.GetPackageCount()\n\t\tif packageCount == None:\n\t\t\tlogging.critical (\"dbus PackageManager Settings not set up -- can't continue\")\n\t\t\treturn False\n\t\ti = 0\n\t\twhile i < packageCount:\n\t\t\t# no package name tells PackageClas init to pull package name from dbus\n\t\t\tcls.PackageList.append (PackageClass ( section = i ) )\n\t\t\ti += 1\n\t\treturn True\n\n\n\t#\tPackageNameValid\n\t# checks the package name to see if it is valid\n\t#\n\t# invalid names contain strings in the rejectStrings\n\t#\tor complete names in rejectNames\n\t#\tor names beginning with '.'\n\t#\n\t# returns true if name is OK, false if in the reject list\n\n\trejectStrings = [ \"-current\", \"-latest\", \"-main\", \"-test\", \"-temp\", \"-debug\", \"-beta\", \"-backup1\", \"-backup2\",\n\t\t\t\t\t\"-blind\", \"-0\", \"-1\", \"-2\", \"-3\", \"-4\", \"-5\", \"-6\", \"-7\", \"-8\", \"-9\", \"ccgx\", \" \" ]\n\n\trejectNames = [ \"conf\", \"db\", \"etc\", \"home\", \"keys\", \"log\", \"lost+found\", \"setupOptions\", \"themes\", \"tmp\", \"var\", \"venus\", \"vrmfilescache\" ]\n\n\t@classmethod\n\tdef PackageNameValid (cls, packageName):\n\n\t\tif packageName == None or packageName == \"\":\n\t\t\treturn False\n\n\t\tif packageName[0] == '.':\n\t\t\treturn False\n\n\t\tfor reject in cls.rejectNames:\n\t\t\tif reject == packageName:\n\t\t\t\treturn False\n\n\t\tfor reject in cls.rejectStrings:\n\t\t\tif reject in packageName:\n\t\t\t\treturn False\n\n\t\treturn True\n\n\n\t#\tAddStoredPackages\n\t# add packages stored in /data to the package list\n\t# in order to qualify as a package:\n\t#\tmust be a directory\n\t#\tname must not contain strings in the reject lists\n\t#\tname must not include any spaces\n\t#\tdirectory must contain a file named setup\n\t#\tdiretory must contain a file named version\n\t#\tfirst character of version file must be 'v'\n\t#\tname must be unique - that is not match any existing packages\n\t# order of validating tests minimizes execution time (determined emperically)\n\t#\n\t# AddStoredPackages is called from init\n\t#\tand the AddPackages run () loop for background updates\n\n\t@classmethod\n\tdef AddStoredPackages (cls):\n\t\tglobal Platform\n\n\t\tplatformIsRaspberryPi = Platform[0:4] == 'Rasp'\n\n\t\tfor packageName in os.listdir (\"/data\"):\n\t\t\tif not PackageClass.PackageNameValid (packageName):\n\t\t\t\tcontinue\n\t\t\t# if package is already in the active list - skip it\n\t\t\tDbusIf.LOCK (\"AddStoredPackages\")\n\t\t\tpackage = PackageClass.LocatePackage (packageName)\n\t\t\tDbusIf.UNLOCK (\"AddStoredPackages\")\t\t\t\n\t\t\tif package != None:\n\t\t\t\tcontinue\n\n\t\t\tpackageDir = \"/data/\" + packageName\n\n\t\t\t# skip if no setup file - also verifies packageDir is a directory!\n\t\t\tif not os.path.exists (packageDir + \"/setup\"):\n\t\t\t\tcontinue\n\n\t\t\t# skip if no version file or not a valid version\n\t\t\tversionFile = packageDir + \"/version\"\n\t\t\ttry:\n\t\t\t\tfd = open (versionFile, 'r')\n\t\t\t\tversion = fd.readline().strip()\n\t\t\t\tfd.close ()\n\t\t\texcept:\n\t\t\t\tcontinue\n\t\t\tif version == \"\" or version[0] != 'v':\n\t\t\t\tcontinue\n\t\t\t# skip if package is for Raspberry PI only and platform is not\n\t\t\tif os.path.exists (packageDir + \"/raspberryPiOnly\") and not platformIsRaspberryPi:\n\t\t\t\tcontinue\n\t\t\t# skip if package was manually removed\n\t\t\tif not PackageClass.GetAutoAddOk (packageName):\n\t\t\t\tcontinue\n\t\t\t# package is unique and passed all tests - schedule the package addition\n\t\t\tPushAction ( command='add:' + packageName, source='AUTO')\n\n\n\t# AddPackage adds one package to the package list\n\t# packageName must be specified\n\t# the package names must be unique\n\t#\n\t# this method is called from the GUI add package command\n\n\t@classmethod\n\tdef AddPackage ( cls, packageName=None, gitHubUser=None, gitHubBranch=None, source=None ):\n\t\tif source == 'GUI':\n\t\t\treportStatusTo = 'Editor'\n\t\t# AUTO or DEFAULT source\n\t\telse:\n\t\t\treportStatusTo = None\n\n\t\tif packageName == None or packageName == \"\":\n\t\t\tDbusIf.UpdateStatus ( message=\"no package name for AddPackage - nothing done\",\n\t\t\t\t\t\t\twhere=reportStatusTo, logLevel=ERROR )\n\t\t\tif source == 'GUI':\n\t\t\t\tDbusIf.AcknowledgeGuiEditAction ( 'ERROR' )\n\t\t\treturn False\n\n\t\t# insure packageName is unique before adding this new package\n\t\tsuccess = False\n\t\tDbusIf.LOCK (\"AddPackage\")\n\t\tpackage = PackageClass.LocatePackage (packageName)\n\n\t\t# new packageName is unique, OK to add it\n\t\tif package == None:\n\t\t\tDbusIf.UpdateStatus ( message=\"adding \" + packageName, where='Editor', logLevel=INFO )\n\n\t\t\tsection = len(cls.PackageList)\n\t\t\tcls.PackageList.append( PackageClass ( section, packageName = packageName ) )\n\t\t\tDbusIf.UpdatePackageCount ()\n\t\t\tsuccess = True\n\n\t\t\t# add user/branch from caller\n\t\t\tpackage = PackageClass.LocatePackage (packageName)\n\t\t\tif package != None:\n\t\t\t\tif gitHubUser == None:\n\t\t\t\t\t\tgitHubUser = \"?\"\n\t\t\t\tif gitHubBranch == None:\n\t\t\t\t\t\tgitHubBranch = \"?\"\n\t\t\t\tpackage.SetGitHubUser (gitHubUser)\n\t\t\t\tpackage.SetGitHubBranch (gitHubBranch)\n\n\t\t\tif source == 'GUI':\n\t\t\t\tDbusIf.AcknowledgeGuiEditAction ( '' )\n\t\t\t\tDbusIf.UpdateStatus ( message = \"\", where='Editor')\n\n\t\t\t# allow auto adds and auto installs\n\t\t\tPackageClass.SetAutoAddOk (packageName, True)\n\t\t\tpackage.SetAutoInstallOk (True)\n\n\t\telse:\n\t\t\tif source == 'GUI':\n\t\t\t\tDbusIf.UpdateStatus ( message=packageName + \" already exists - choose another name\", where=reportStatusTo, logLevel=INFO )\n\t\t\t\tDbusIf.AcknowledgeGuiEditAction ( 'ERROR' )\n\t\t\telse:\n\t\t\t\tDbusIf.UpdateStatus ( message=packageName + \" already exists\", where=reportStatusTo, logLevel=WARNING )\n\t\t\n\t\tDbusIf.UNLOCK (\"AddPackage\")\n\t\treturn success\n\t# end AddPackage\n\n\n\t# packages are removed as a request from the GUI (packageName specified)\n\t# or during system initialization (packageIndex specified)\n\t# to remove a package:\n\t#\t1) locate the entry matching package name  (if any)\n\t#\t2) move all packages after that entry to the previous slot (if any)\n\t#\t3) erase the last package slot to avoid confusion (by looking at dbus-spy)\n\t#\t3) remove the entry in PackageList (pop)\n\t#\t4) update the package count\n\t#\t5) set DO_NOT_AUTO_ADD flag file to prevent\n\t#\t\tpackage from being re-added to the package list\n\t#\t\tflag file is deleted when package is manually installed again\n\t#\n\t#\tRemove package must be passed either the package name or an index into PackageList\n\t#\n\t#\treturns True if package was removed, False if not\n\t#\n\t#\tthis is all done while the package list is locked !!!!\n\n\t@classmethod\n\tdef RemovePackage (cls, packageName=None, packageIndex=None, isDuplicate=False ):\n\t\t# packageName specified so this is a call from the GUI\n\t\tif packageName != None:\n\t\t\tguiRequestedRemove = True\n\t\t\tif packageName == \"SetupHelper\":\n\t\t\t\tDbusIf.UpdateStatus ( message=\"REMOVING SetupHelper\" + packageName, where='Editor', logLevel=CRITICAL )\n\t\t\telse:\n\t\t\t\tDbusIf.UpdateStatus ( message=\"removing \" + packageName, where='Editor', logLevel=INFO )\n\t\t# no package name specified, so this is a call from system initialization - messages to log only\n\t\telif packageIndex != None:\n\t\t\tguiRequestedRemove = False\n\t\t\tname = PackageClass.PackageList [packageIndex].PackageName\n\t\t\tif name == None or name == \"\":\n\t\t\t\tlogging.error ( \"RemovePackage: removing package without a name\" )\n\t\t\telse:\n\t\t\t\tlogging.info ( \"RemovePackage: removing \" + name )\n\t\t# neither package name nor package instance passed - can't do anything\n\t\telse:\n\t\t\tlogging.error ( \"RemovePackage: no package info passed - nothing done\" )\n\t\t\treturn\n\n\t\tDbusIf.LOCK (\"RemovePackage\")\n\t\tpackages = PackageClass.PackageList\n\t\tlistLength = len (packages)\n\t\tif listLength == 0:\n\t\t\tDbusIf.UNLOCK (\"RemovePackage\")\n\t\t\treturn\n\n\t\t# locate index of packageName\n\t\t#\tLocaatePackage not used because we want the index anyway\n\t\tif guiRequestedRemove:\n\t\t\ttoIndex = 0\n\t\t\tmatchFound = False\n\t\t\twhile toIndex < listLength:\n\t\t\t\tif packageName == packages[toIndex].PackageName:\n\t\t\t\t\tmatchFound = True\n\t\t\t\t\tbreak\n\t\t\t\ttoIndex += 1\n\t\t# called from init - already have index\n\t\telse:\n\t\t\ttoIndex = packageIndex\n\t\t\tmatchFound = True\n\n\t\tpackageIsInstalled = packages[toIndex].InstalledVersion != \"\"\n\t\t\n\t\t# if package is installed, don't remove it\n\t\tif matchFound and not packageIsInstalled:\n\t\t\t# if not just removing a duplicate\n\t\t\t# block future automatic adds since the package is being removed\n\t\t\tif not isDuplicate:\n\t\t\t\tPackageClass.SetAutoAddOk (packageName, False)\n\n\t\t\t# move packages after the one to be remove down one slot (copy info)\n\t\t\t# each copy overwrites the lower numbered package\n\t\t\tfromIndex = toIndex + 1\n\t\t\twhile fromIndex < listLength:\n\t\t\t\t# dbus Settings\n\t\t\t\ttoPackage = packages[toIndex]\n\t\t\t\tfromPackage = packages[fromIndex]\n\t\t\t\ttoPackage.SetPackageName (fromPackage.PackageName )\n\t\t\t\ttoPackage.SetGitHubUser (fromPackage.GitHubUser )\n\t\t\t\ttoPackage.SetGitHubBranch (fromPackage.GitHubBranch )\n\n\t\t\t\t# dbus service params\n\t\t\t\ttoPackage.SetGitHubVersion (fromPackage.GitHubVersion )\n\t\t\t\ttoPackage.SetPackageVersion (fromPackage.PackageVersion )\n\t\t\t\ttoPackage.SetInstalledVersion (fromPackage.InstalledVersion )\n\t\t\t\ttoPackage.SetIncompatible (fromPackage.Incompatible, fromPackage.IncompatibleDetails,\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tfromPackage.IncompatibleResolvable )\n\n\t\t\t\t# package variables\n\t\t\t\ttoPackage.DownloadPending = fromPackage.DownloadPending\n\t\t\t\ttoPackage.InstallPending = fromPackage.InstallPending\n\t\t\t\ttoPackage.AutoInstallOk = fromPackage.AutoInstallOk\n\t\t\t\ttoPackage.DependencyErrors = fromPackage.DependencyErrors\n\t\t\t\ttoPackage.FileConflicts = fromPackage.FileConflicts\n\t\t\t\ttoPackage.LastPatchErrorUpdate = fromPackage.LastPatchErrorUpdate\n\t\t\t\ttoPackage.lastScriptPrecheck = fromPackage.lastScriptPrecheck\n\t\t\t\ttoPackage.lastGitHubRefresh = fromPackage.lastGitHubRefresh\n\t\t\t\ttoPackage.ActionNeeded = fromPackage.ActionNeeded\n\n\t\t\t\ttoIndex += 1\n\t\t\t\tfromIndex += 1\n\n\t\t\t# here, toIndex points to the last package in the old list\n\t\t\ttoPackage = packages[toIndex]\n\n\t\t\t# can't actually remove service paths cleanly\n\t\t\t#\tso just set contents to null/False\n\t\t\t# \tthey will disappear after PackageManager is started the next time\n\t\t\ttoPackage.SetGitHubVersion (\"?\")\n\t\t\ttoPackage.SetInstalledVersion (\"?\")\n\t\t\ttoPackage.SetPackageVersion (\"?\")\n\t\t\ttoPackage.SetIncompatible (\"\")\n\t\t\ttoPackage.LastPatchErrorUpdate = 0\n\t\t\ttoPackage.lastScriptPrecheck = 0\n\t\t\ttoPackage.lastGitHubRefresh = 0\n\t\t\ttoPackage.ActionNeeded = NONE\n\t\t\t\n\n\t\t\t# remove the Settings and service paths for the package being removed\n\t\t\tDbusIf.RemoveDbusSettings ( [toPackage.packageNamePath, toPackage.gitHubUserPath, toPackage.gitHubBranchPath] )\n\n\t\t\t# remove entry from package list\n\t\t\tpackages.pop (toIndex)\n\t\t\tDbusIf.UpdatePackageCount ()\n\t\tDbusIf.UNLOCK (\"RemovePackage\")\n\t\t# this package was manually removed so block automatic adds\n\t\t#\tin the package directory\n\t\tif guiRequestedRemove:\n\t\t\tif matchFound:\n\t\t\t\t# block automatic adds\n\t\t\t\tPackageClass.SetAutoAddOk (packageName, False)\n\n\t\t\t\tDbusIf.UpdateStatus ( message=\"\", where='Editor' )\n\t\t\t\tDbusIf.AcknowledgeGuiEditAction ( '' )\n\t\t\telse:\n\t\t\t\tDbusIf.UpdateStatus ( message=packageName + \" not removed - name not found\", where='Editor', logLevel=ERROR )\n\t\t\t\tDbusIf.AcknowledgeGuiEditAction ( 'ERROR' )\n\t\treturn matchFound\n\t# end RemovePackage\n\n\n\t#\tUpdateVersionsAndFlags\n\t#\n\t# retrieves packages versions from the file system\n\t#\teach package contains a file named version in it's root directory\n\t#\t\tthat becomes packageVersion\n\t#\tthe installedVersion-... file is associated with installed packages\n\t#\t\tabesense of the file indicates the package is not installed\n\t#\t\tpresense of the file indicates the package is installed\n\t#\t\tthe content of the file is the actual version installed\n\t#\t\tin prevous versions of the setup scripts, this file could be empty, \n\t#\t\tso we show this as \"unknown\"\n\t#\n\t# also sets incompatible parameter and AutoInstallOk local variable to save time in other loops\n\t#\n\t# the single package variation is broken out so it can be called from other methods\n\t#\tto insure version information is up to date before proceeding with an operaiton\n\t#\n\t# must be called while LOCKED !!\n\n\tdef UpdateVersionsAndFlags (self, doConflictChecks=False, doScriptPreChecks=False):\n\t\tglobal VersionToNumber\n\t\tglobal VenusVersion\n\t\tglobal VenusVersionNumber\n\t\tglobal Platform\n\n\t\tpackageName = self.PackageName\n\n\t\t# fetch installed version\n\t\tinstalledVersionFile = \"/etc/venus/installedVersion-\" + packageName\n\t\ttry:\n\t\t\tversionFile = open (installedVersionFile, 'r')\n\t\texcept:\n\t\t\tinstalledVersion = \"\"\n\t\telse:\n\t\t\tinstalledVersion = versionFile.readline().strip()\n\t\t\tversionFile.close()\n\t\t\t# if file is empty, an unknown version is installed\n\t\t\tif installedVersion ==  \"\":\n\t\t\t\tinstalledVersion = \"unknown\"\n\t\tself.SetInstalledVersion (installedVersion)\n\n\t\tpackageDir = \"/data/\" + packageName\n\n\t\t# no package directory - null out all params\n\t\tif not os.path.isdir (packageDir):\n\t\t\tself.SetPackageVersion (\"\")\n\t\t\tself.AutoInstallOk = False\n\t\t\tself.SetIncompatible (\"no package\")\n\t\t\treturn\n\n\t\t# fetch package version (the one in /data/packageName)\n\t\ttry:\n\t\t\tversionFile = open (packageDir + \"/version\", 'r')\n\t\t\tpackageVersion = versionFile.readline().strip()\n\t\t\tversionFile.close()\n\t\texcept:\n\t\t\tpackageVersion = \"\"\n\t\tself.SetPackageVersion (packageVersion)\n\n\t\tcompatible = True\n\n\t\t# set the incompatible parameter\n\t\t#\tto 'PLATFORM' or 'VERSION'\n\t\tif os.path.exists (packageDir + \"/raspberryPiOnly\" ):\n\t\t\tif Platform[0:4] != 'Rasp':\n\t\t\t\tself.SetIncompatible (\"incompatible with \" + Platform)\n\t\t\t\tcompatible = False\n\t\t\t\tdoConflictChecks = False\n\n\t\t# update local auto install flag based on DO_NOT_AUTO_INSTALL\n\t\tflagFile = \"/data/setupOptions/\" + packageName + \"/DO_NOT_AUTO_INSTALL\"\n\t\tif os.path.exists (flagFile):\n\t\t\tself.AutoInstallOk = False\n\t\telse:\n\t\t\tself.AutoInstallOk = True\n\n\t\t# platform is OK, now check versions\n\t\tif compatible:\n\t\t\ttry:\n\t\t\t\tfd = open (packageDir + \"/firstCompatibleVersion\", 'r')\n\t\t\t\tfirstVersion = fd.readline().strip()\n\t\t\t\tfd.close ()\n\t\t\texcept:\n\t\t\t\tfirstVersion = \"v2.71\"\n\t\t\ttry:\n\t\t\t\tfd = open (packageDir + \"/obsoleteVersion\", 'r')\n\t\t\t\tobsoleteVersion = fd.readline().strip()\n\t\t\t\tfd.close ()\n\t\t\texcept:\n\t\t\t\tobsoleteVersion = \"v9999.9999.9999\"\n\t\t\t\n\t\t\tfirstVersionNumber = VersionToNumber (firstVersion)\n\t\t\tobsoleteVersionNumber = VersionToNumber (obsoleteVersion)\n\t\t\tif VenusVersionNumber < firstVersionNumber or VenusVersionNumber >= obsoleteVersionNumber:\n\t\t\t\tself.SetIncompatible (\"incompatible with \" + VenusVersion)\n\t\t\t\tcompatible = False\n\t\t\t\tdoConflictChecks = False\n\t\t\telif os.path.exists (packageDir + \"/validFirmwareVersions\"):\n\t\t\t\twith open(packageDir + \"/validFirmwareVersions\") as f:\n\t\t\t\t\tlines = f.readlines ()\n\t\t\t\t\tversionPresent = False\n\t\t\t\t\tfor line in lines:\n\t\t\t\t\t\tif line.strip() == VenusVersion:\n\t\t\t\t\t\t\tversionPresent = True\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\tif not versionPresent:\n\t\t\t\t\t\tself.SetIncompatible (\"incompatible with \" + VenusVersion)\n\t\t\t\t\t\tcompatible = False\n\t\t\t\t\t\tdoConflictChecks = False\n\n\t\t# check to see if command line is needed for install\n\t\t# the optionsRequired flag in the package directory indicates options must be set before a blind install\n\t\t# the optionsSet flag indicates the options HAVE been set already\n\t\t# so if optionsRequired == True and optionsSet == False, can't install from GUI\n\t\tif compatible:\n\t\t\tif os.path.exists (\"/data/\" + packageName + \"/optionsRequired\" ):\n\t\t\t\tif not os.path.exists ( \"/data/setupOptions/\" + packageName + \"/optionsSet\"):\n\t\t\t\t\tself.SetIncompatible (\"install from command line\" )\n\t\t\t\t\tcompatible = False\n\t\t\t\t\tdoConflictChecks = False\n\n\t\t# check to see if file set has errors\n\t\tif compatible:\n\t\t\tfileSetsDir = packageDir + \"/FileSets\"\n\t\t\tfileSet = fileSetsDir + \"/\" + VenusVersion\n\t\t\tif os.path.exists (fileSet + \"/INCOMPLETE\"):\n\t\t\t\tself.SetIncompatible (\"incomplete file set for \" + str (VenusVersion) )\n\t\t\t\tcompatible = False\n\t\t\t\tdoConflictChecks = False\n\n\n\t\t# check for package conflicts - but not if an operation is in progress\n\t\tif doConflictChecks and not self.InstallPending and not self.DownloadPending:\n\t\t\t# update dependencies\n\t\t\tdependencyFile = \"/data/\" + packageName + \"/packageDependencies\"\n\t\t\tdependencyErrors = []\n\t\t\tif os.path.exists (dependencyFile):\n\t\t\t\ttry:\n\t\t\t\t\twith open (dependencyFile, 'r') as file:\n\t\t\t\t\t\tfor item in file:\n\t\t\t\t\t\t\tparts = item.split ()\n\t\t\t\t\t\t\tif len (parts) < 2:\n\t\t\t\t\t\t\t\tlogging.error (\"package dependency \" + item + \" incomplete\")\n\t\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t\tdependencyPackage = parts [0]\n\t\t\t\t\t\t\tdependencyRequirement = parts [1]\n\n\t\t\t\t\t\t\tinstalledFile = \"/etc/venus/installedVersion-\" + dependencyPackage\n\t\t\t\t\t\t\tpackageIsInstalled = os.path.exists (installedFile)\n\t\t\t\t\t\t\tpackageMustBeInstalled = dependencyRequirement == \"installed\"\n\t\t\t\t\t\t\tif packageIsInstalled != packageMustBeInstalled:\n\t\t\t\t\t\t\t\tdependencyErrors.append ( (dependencyPackage, dependencyRequirement) )\n\t\t\t\t\tdependencyErrors.sort()\n\t\t\t\texcept:\n\t\t\t\t\tpass\n\t\t\t# log dependency changes if they have changed\n\t\t\tif dependencyErrors != self.DependencyErrors:\n\t\t\t\tself.DependencyErrors = dependencyErrors\n\t\t\t\tif len (dependencyErrors) > 0:\n\t\t\t\t\tfor dependency in dependencyErrors:\n\t\t\t\t\t\t(dependencyPackage, dependencyRequirement) = dependency\n\t\t\t\t\t\tlogging.info (packageName + \" requires \" + dependencyPackage + \" to be \" + dependencyRequirement)\n\t\t\t\telse:\n\t\t\t\t\tlogging.info (\"dependency conflicts for \" + packageName + \" have been resolved\")\n\n\t\t\t# check for file conflicts with prevously installed packages\n\t\t\t# each line in all file lists are checked to see if the <active file>.package contains a different package name\n\t\t\t# if they differ, a conflict between this and the other package exists\n\t\t\t#\trequiring one or the other package to be uninstalled\n\t\t\t#\n\t\t\t# patched files are NOT checked because they patch the active file\n\t\t\t# the setup script with the 'check' option will test the patch file\n\t\t\t#\tand report any patch failures\n\n\t\t\tfileConflicts = []\n\t\t\tfileLists =  [ \"fileList\", \"fileListVersionIndependent\" ]\n\t\t\tfor fileList in fileLists:\n\t\t\t\tpath = \"/data/\" + packageName + \"/FileSets/\" + fileList\n\t\t\t\tif not os.path.exists (path):\n\t\t\t\t\tcontinue\n\t\t\t\ttry:\n\t\t\t\t\twith open (path, 'r') as file:\n\t\t\t\t\t\t# valid entries begin with / and everything after white space is discarded\n\t\t\t\t\t\t# the result should be a full path to one replacment file\n\t\t\t\t\t\tfor entry in file:\n\t\t\t\t\t\t\tentry = entry.strip ()\n\t\t\t\t\t\t\tif not entry.startswith (\"/\"):\n\t\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t\treplacementFile = entry.split ()[0].strip ()\n\t\t\t\t\t\t\tpackagesList = replacementFile + \".package\"\n\t\t\t\t\t\t\tif not os.path.exists ( packagesList ) :\n\t\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t\t# if a package list for an active file changes,\n\t\t\t\t\t\t\t#\trun script checks again to uncover new or resolved conflicts\n\t\t\t\t\t\t\tif os.path.getmtime (packagesList) > self.lastScriptPrecheck:\n\t\t\t\t\t\t\t\tdoScriptPreChecks = True\n\t\t\t\t\t\t\ttry:\n\t\t\t\t\t\t\t\twith open (packagesList, 'r') as plFile:\n\t\t\t\t\t\t\t\t\tfor entry2 in plFile:\n\t\t\t\t\t\t\t\t\t\tpackageFromList = entry2.strip()\n\t\t\t\t\t\t\t\t\t\t# here if previously updated file was from a different package\n\t\t\t\t\t\t\t\t\t\tif packageFromList != packageName:\n\t\t\t\t\t\t\t\t\t\t\tfile =  os.path.basename (replacementFile)\n\t\t\t\t\t\t\t\t\t\t\tfileConflicts.append ( (packageFromList, \"uninstalled\", file) )\n\t\t\t\t\t\t\texcept:\n\t\t\t\t\t\t\t\tpass\n\t\t\t\texcept:\n\t\t\t\t\tlogging.critical (\"error while reading file lists for \" + packageName)\n\t\t\t\t\tcontinue\n\n\t\t\tconflicts = self.DependencyErrors\n\n\t\t\tif fileConflicts != self.FileConflicts:\n\t\t\t\tself.FileConflicts = fileConflicts\n\t\t\t\tif len (fileConflicts) > 0:\n\t\t\t\t\tfor (otherPackage, dependency, file) in fileConflicts:\n\t\t\t\t\t\tlogging.info (\"to install \" + packageName + \", \" + otherPackage + \" must not be installed (\" + file + \")\" )\n\t\t\t\t\t\tconflicts.append ( ( otherPackage, dependency ) )\n\t\t\t\telse:\n\t\t\t\t\tlogging.info (\"file conflicts for \" + packageName + \" have been resolved\")\n\n\t\t\tdetails = \"\"\n\t\t\tif len (conflicts) > 0:\n\t\t\t\t# eliminate duplicates\n\t\t\t\tconflicts = list ( set ( conflicts ) )\n\t\t\t\tresolveOk = True\n\t\t\t\tfor ( otherPackage, dependency ) in conflicts:\n\t\t\t\t\tif dependency == \"uninstalled\":\n\t\t\t\t\t\tdetails += otherPackage + \" must not be installed\\n\"\n\t\t\t\t\telse:\n\t\t\t\t\t\tconflictPackage = PackageClass.LocatePackage (otherPackage)\n\t\t\t\t\t\tif conflictPackage == None:\n\t\t\t\t\t\t\tdetails += otherPackage + \" must be installed but not available\\n\"\n\t\t\t\t\t\t\tresolveOk = False\n\t\t\t\t\t\telif conflictPackage.PackageVersion != \"\":\n\t\t\t\t\t\t\tdetails += otherPackage + \" must be installed\\n\"\n\t\t\t\t\t\telif conflictPackage.GitHubVersion != \"\":\n\t\t\t\t\t\t\tdetails += otherPackage + \" must be downloaded and installed\\n\"\n\t\t\t\t\t\telse:\n\t\t\t\t\t\t\tdetails += otherPackage + \" unknown\\n\"\n\t\t\t\tself.SetIncompatible (\"package conflict\", details, resolvable=resolveOk)\n\t\t\t\tcompatible = False\n\n\t\t\t# make sure script checks are run once at boot\n\t\t\t#\t(eg patched errors, but there are others)\n\t\t\tif self.lastScriptPrecheck == 0:\n\t\t\t\tdoScriptPreChecks = True\n\t\t\tself.lastScriptPrecheck = time.time ()\n\t\t# end if doConflictChecks\n\n\t\t# force patch error rebuild if other errors used Incompatible fields\n\t\tif not compatible or self.DownloadPending:\n\t\t\tself.LastPatchErrorUpdate = 0\n\t\t# check for and report patch errors if there are no other errors\n\t\telif compatible and not self.InstallPending and not self.DownloadPending:\n\t\t\tif os.path.exists (packageDir + \"/patchErrors\"):\n\t\t\t\tcompatible = False\n\t\t\t\tlastPatchErrorUpdate = os.path.getmtime (packageDir + \"/patchErrors\")\n\t\t\t\t# if patchErrors file has updated, rebuild error list\n\t\t\t\tif lastPatchErrorUpdate != self.LastPatchErrorUpdate:\n\t\t\t\t\tself.LastPatchErrorUpdate = lastPatchErrorUpdate\n\t\t\t\t\tdetails = \"\"\n\t\t\t\t\tpatchCheckErrors = []\n\t\t\t\t\twith open ( packageDir + \"/patchErrors\" ) as file:\n\t\t\t\t\t\tfor line in file:\n\t\t\t\t\t\t\tpatchCheckErrors.append ( line )\n\t\t\t\t\tpatchCheckErrors = list ( set ( patchCheckErrors ) )\n\t\t\t\t\tif len (patchCheckErrors) > 0:\n\t\t\t\t\t\tfor line in patchCheckErrors:\n\t\t\t\t\t\t\tpatchFailure = line.strip()\n\t\t\t\t\t\t\tdetails += patchFailure + \"\\n\"\n\t\t\t\t\t\t\tlogging.warning (packageName + \" patch check error: \" + patchFailure + \" \")\n\t\t\t\t\telse:\n\t\t\t\t\t\tlogging.info (packageName + \" patch check reported no errors\")\n\t\t\t\t\tself.SetIncompatible (\"patch error\", details )\n\t\t\telse:\n\t\t\t\tself.LastPatchErrorUpdate = 0\n\n\t\t# if no incompatibilities found, clear incompatible dbus parameters\n\t\t#\tso the GUI will allow installs\n\t\tif compatible:\n\t\t\tself.SetIncompatible (\"\")\n\n\t\t# run setup script to check for file conflicts (can't be checked here)\n\t\tif doScriptPreChecks and os.path.exists (\"/data/\" + packageName + \"/setup\"):\n\t\t\tPushAction ( command='check' + ':' + packageName, source='AUTO' )\n\t# end UpdateVersionsAndFlags\n# end Package\n\n\n#\tUpdateGitHubVersionClass\n#\n# downloads the GitHub versions\n# this work is done in a separate thread so network activity can be spaced out\n#\tand isolated from other acvities since they may take a while on slow networks\n# \n# a message is used to trigger a priority update for a specific package\n#\t\tthis is used when the operator changes GitHub user/branch so the version\n#\t\t\tupdates rapidly\n#\tor speed up the refresh rate\n# a STOP message is used to wake the thread\n#\tso that the tread can exit wihout waitin for a potentially long timeout\n#\n# background refreshes are triggered by a queue timeout while waiting for messages\n#\trefreshes occur rapidly to refresh the GUI and minimize time waiting for the versions\n#\tor are spaced out over time such that version information is refreshed every 10 seconds\n#\n#\tInstances:\n#\t\tUpdateGitHubVersion (a separate thread)\n#\n#\tMethods:\n#\t\tupdateGitHubVersion \n#\t\trun ()\n#\t\tStopThread ()\n#\n# delay for GitHub version refreshes\n# slow refresh also controls GitHub version expiration\n\nFAST_GITHUB_REFRESH = 0.25\nNORMAL_GITHUB_REFRESH = 600.0\t# 10 minutes\nHOURLY_GITHUB_REFRESH = 60.0 * 60.0\nDAILY_GITHUB_REFRESH = HOURLY_GITHUB_REFRESH * 24.0\n\nclass UpdateGitHubVersionClass (threading.Thread):\n\n\t#\tupdateGitHubVersion\n\t#\n\t# fetches the GitHub version from the internet and stores it in the package\n\t#\n\t# this is called from the background thread run () below\n\t#\n\t# if the wget fails, the GitHub version is set to \"\"\n\t# this will happen if the name, user or branch are not correct\n\t# or if there is no internet connection\n\t#\n\t# the package GitHub version is upated\n\t# but the version is also returned to the caller\n\t#\n\t#\tInstances:\n\t#\t\tUpdateGitHubVersion (thread)\n\t#\n\t#\tMethods:\n\t#\t\tupdateGitHubVersion\n\t#\t\tSetPriorityGitHubVersion\n\t#\t\trun (the thread)\n\n\tdef updateGitHubVersion (self, packageName, gitHubUser, gitHubBranch):\n\n\t\turl = \"https://raw.githubusercontent.com/\" + gitHubUser + \"/\" + packageName + \"/\" + gitHubBranch + \"/version\"\n\t\ttry:\n\t\t\tproc = subprocess.Popen ([\"wget\", \"--timeout=10\", \"-qO\", \"-\", url],\n\t\t\t\t\t\tbufsize=-1, stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n\t\t\tstdout, _ = proc.communicate ()\n\t\t\tstdout = stdout.decode ().strip ()\n\t\t\treturnCode = proc.returncode\n\t\texcept:\n\t\t\tlogging.error (\"wget for version failed \" + packageName)\n\t\t\tgitHubVersion = \"\"\n\t\telse:\n\t\t\tif proc.returncode == 0:\n\t\t\t\tgitHubVersion = stdout\n\t\t\telse:\n\t\t\t\tgitHubVersion = \"\"\n\n\t\t# locate the package with this name and update it's GitHubVersion\n\t\t# if not in the list discard the information\n\t\tDbusIf.LOCK (\"updateGitHubVersion\")\n\t\tpackage = PackageClass.LocatePackage (packageName)\n\t\tif package != None:\n\t\t\tpackage.SetGitHubVersion (gitHubVersion)\n\t\t\tpackage.lastGitHubRefresh = time.time ()\n\t\tDbusIf.UNLOCK (\"updateGitHubVersion\")\n\t\treturn gitHubVersion\n\n\n\tdef __init__(self):\n\t\tthreading.Thread.__init__(self)\n\t\tself.GitHubVersionQueue = queue.Queue (maxsize = 50)\n\t\tself.threadRunning = True\n\t\t# package needing immediate update\n\t\tself.priorityPackageName = None\n\n\n\t#\tSetPriorityGitHubVersion\n\t# pushes a priority package version update onto our queue\n\t#\n\t# 'local' is a dummy source for the queue pull\n\t\n\tdef SetPriorityGitHubVersion (self, command):\n\t\tself.GitHubVersionQueue.put ( (command, 'local'), block=False )\n\n\n\t#\tUpdateGitHubVersion run ()\n\t#\n\t# updates GitHub versions\n\t# GitHub access is spaced out to minimize network traffic\n\t# a priority update is pushed onto our que when GitHub info changes\n\t#\n\t# StopThread () is called to shut down the thread when PackageManager is quitting\n\t# \"STOP\" is pushed on to the queue by StopThread to cause the run thread to detect\n\t#\tdetect the stop request immediately\n\t#\n\t# run () blocks on reading from our queue\n\t#\tthe timeout for the queue pull paces the version fetches\n\t#\tnormally, the timeout will occur with the queue empty\n\t#\t\tin which case we update the next GitHub version for the next package\n\t#\tthe time between version fetches changes\n\t#\t\tfor the first pass, a shorter delay is used\n\t#\t\tafter all packages have been updated, the delay is increased\n\t#\t\tthe shorter delay is used again when we pull \"REFRESH\" off the queue\n\t#\n\t#\twhen the que returns an item, it is checked to see if it is either a\n\t#\t\tprioirty package to update it's GitHub version \n\t#\t\t\"STOP\" - indicating run should return\n\t#\t\t\"REFRESH\" - indicating the loop should update all package GitHub versions\n\t#\t\t\tthis is used when download refresh mode/rates change\n\t#\t\t\"ALL\" - same as REFRESH but from the GUI\n\t#\t\t\t(uses the saem message path from GUI as the prioirty package update)\n\t#\t\t\twhen entering the Active packages menu\n\t#\t\t\tneed to clear the dbus /GuiEditAction\n\t#\t\tchecks the threadRunning flag and returns if it is False,\n\t#\twhen run returns, the main method should catch the tread with join ()\n\n\tdef StopThread (self):\n\t\tself.threadRunning = False\n\t\tself.SetPriorityGitHubVersion ( 'STOP' )\n\n\n\tdef run (self):\n\t\tglobal WaitForGitHubVersions\n\n\t\tgitHubVersionPackageIndex = 0\n\t\tforcedRefresh = True\n\n\t\tpackageListLength = 0\n\t\t\n\t\twhile self.threadRunning:\n\t\t\tdownloadMode = DbusIf.GetAutoDownloadMode ()\n\n\t\t\t# do initial refreshes quickly\n\t\t\tif forcedRefresh:\n\t\t\t\tdelay = FAST_GITHUB_REFRESH\n\t\t\t# otherwise set delay to complete scan of all versions in the selected refresh period\n\t\t\t#\tthis prevents GitHub versions from going undefined if refreshes are happening\n\t\t\telse:\n\t\t\t\tif downloadMode == NORMAL_DOWNLOAD:\n\t\t\t\t\tdelay = NORMAL_GITHUB_REFRESH\n\t\t\t\telif downloadMode == HOURLY_DOWNLOAD:\n\t\t\t\t\tdelay = HOURLY_GITHUB_REFRESH\n\t\t\t\telse:\n\t\t\t\t\tdelay = DAILY_GITHUB_REFRESH\n\t\t\t\t#\tthis prevents divide by zero - value not actually used\n\t\t\t\tif packageListLength != 0:\n\t\t\t\t\tdelay /= packageListLength\n\t\t\t# queue gets STOP and REFRESH commands or priority package name\n\t\t\t# empty queue signals it's time for a background update\n\t\t\t# queue timeout is used to pace background updates\n\t\t\tcommand = \"\"\n\t\t\tsource = \"\"\n\t\t\tpackageName = \"\"\n\t\t\ttry:\n\t\t\t\t(command, source) = self.GitHubVersionQueue.get (timeout = delay)\n\t\t\t\tparts = command.split (\":\")\n\t\t\t\tlength = len (parts)\n\t\t\t\tif length >= 1:\n\t\t\t\t\tcommand = parts [0]\n\t\t\t\tif length >= 2:\n\t\t\t\t\tpackageName = parts [1]\n\t\t\texcept queue.Empty:\t# means get() timed out as expected - not an error\n\t\t\t\t# timeout indicates it's time to do a background update\n\t\t\t\tpass\n\t\t\texcept:\n\t\t\t\tlogging.error (\"pull from GitHubVersionQueue failed\")\n\t\t\tif command == 'STOP' or self.threadRunning == False:\n\t\t\t\treturn\n\n\t\t\tdoUpdate = False\n\t\t\tpackageUpdate = False\n\n\t\t\t# the REFRESH command triggers a refresh of all pachage Git Hub versions\n\t\t\t# background scans in the mainLoop are blocked until the refresh is complete\n\t\t\tif command == 'REFRESH':\n\t\t\t\tgitHubVersionPackageIndex = 0\n\t\t\t\t# hold off other processing until refresh is complete\n\t\t\t\tWaitForGitHubVersions = True\n\t\t\t\tforcedRefresh = True\t\t# guarantee at least one pass even if auto downloads are off\n\t\t\t# refresh request was received from GUI\n\t\t\telif source == 'GUI':\n\t\t\t\t# acknowledge command ASAP to minimize time GUI is held off\n\t\t\t\tDbusIf.AcknowledgeGuiEditAction ('')\n\t\t\t\tif command != 'gitHubScan':\n\t\t\t\t\tlogging.error (\"incomplete GitHub refresh request from GUI: \" + str(parts))\n\t\t\t\t# if GUI is requesting a refresh of all package versions, trigger a one-time refresh\n\t\t\t\t# but does not block background scans in mainLoop\n\t\t\t\telif packageName == 'ALL':\n\t\t\t\t\tif not forcedRefresh:\n\t\t\t\t\t\tgitHubVersionPackageIndex = 0\n\t\t\t\t\t\tforcedRefresh = True\n\t\t\t\t# refresh is for a spcific package\n\t\t\t\telif packageName != \"\":\n\t\t\t\t\tpackageUpdate = True\n\t\t\t\telse:\n\t\t\t\t\tlogging.error (\"missing name in GitHub refresh request from GUI: \" + str(parts))\n\t\t\t# package priority update NOT from the GUI\n\t\t\telif source == 'local' and packageName != \"\":\n\t\t\t\tpackageUpdate = True\n\t\t\tif packageUpdate:\n\t\t\t\tDbusIf.LOCK (\"UpdateGitHubVersion run 1\")\n\t\t\t\tpackage = PackageClass.LocatePackage (packageName)\n\t\t\t\tif package != None:\n\t\t\t\t\tuser = package.GitHubUser\n\t\t\t\t\tbranch = package.GitHubBranch\n\t\t\t\t\t# always do the update for 'local' source\n\t\t\t\t\tif source != 'GUI':\n\t\t\t\t\t\tdoUpdate = True\n\t\t\t\t\t# for GUI - refresh if no version or last refresh more than 30 seconds ago\n\t\t\t\t\t# prevents unnecessary network traffic when navigating PackageManager menus\n\t\t\t\t\telif package.GitHubVersion == \"\" or time.time () > package.lastGitHubRefresh + 30:\n\t\t\t\t\t\tdoUpdate = True\n\t\t\t\telse:\n\t\t\t\t\tlogging.error (\"can't fetch GitHub version - \" + packageName + \" not in package list\")\n\t\t\t\tDbusIf.UNLOCK (\"UpdateGitHubVersion run 1\")\n\n\t\t\tdoBackground = forcedRefresh or downloadMode != AUTO_DOWNLOADS_OFF\n\t\t\t# no priority update - do background update\n\t\t\tif not doUpdate and doBackground:\n\t\t\t\tDbusIf.LOCK (\"UpdateGitHubVersion run 2\")\n\t\t\t\tpackageListLength = len (PackageClass.PackageList)\n\t\t\t\t# empty package list - no refreshes possible\n\t\t\t\tif packageListLength == 0:\n\t\t\t\t\tgitHubVersionPackageIndex = 0\n\t\t\t\t# select package to update\n\t\t\t\telif gitHubVersionPackageIndex < packageListLength:\n\t\t\t\t\tpackage = PackageClass.PackageList[gitHubVersionPackageIndex]\n\t\t\t\t\tpackageName = package.PackageName\n\t\t\t\t\tuser = package.GitHubUser\n\t\t\t\t\tbranch = package.GitHubBranch\n\t\t\t\t\tdoUpdate = True\n\t\t\t\t\tgitHubVersionPackageIndex += 1\n\t\t\t\t# reached end of list - all package Git Hub versions have been refreshed\n\t\t\t\tif gitHubVersionPackageIndex >= packageListLength:\n\t\t\t\t\tgitHubVersionPackageIndex = 0\n\t\t\t\t\t# notify the main loop that all versions have been refreshed and\n\t\t\t\t\t# download modes can now be changed if appropriate\n\t\t\t\t\tWaitForGitHubVersions = False\n\t\t\t\t\tforcedRefresh = False\n\t\t\t\tDbusIf.UNLOCK (\"UpdateGitHubVersion run 2\")\n\n\t\t\t# do the actual background update outsde the above LOCKED section\n\t\t\t#\tsince the update requires internet access\n\t\t\tif doUpdate:\n\t\t\t\tself.updateGitHubVersion (packageName, user, branch)\n\t\t# end while self.threadRunning\n\t# end UpdateGitHubVersion run ()\n# end UpdateGitHubVersionClass\n\n\n#\tDownloadGitHubPackagesClass\n#\n# downloads packages from GitHub, replacing the existing package\n#\n# downloads can take significant time, so they are handled in a separate thread\n#\n# the GUI and auto download code (in main loop) push download\n#\tactions onto this queue\n#\tthe thread blocks when the queue is empty\n#\n# a STOP command is also pushed onto the queue when PackageManager\n#\tis shutting down. This unblocks this thread which immediately\n#\treads threadRunning. If false, run () returns\n#\n#\tInstances:\n#\t\tDownloadGitHub (a separate thread)\n#\n#\tMethods:\n#\t\tGitHubDownload\n#\t\tDownloadVersionCheck\n#\t\trun\n#\t\tStopThread\n#\n# the run () thread is only responsible for pacing automatic downloads from the internet\n#\tcommands are pushed onto the processing queue (PushAction)\n\nclass DownloadGitHubPackagesClass (threading.Thread):\n\n\tdef __init__(self):\n\t\tthreading.Thread.__init__(self)\n\t\tself.DownloadQueue = queue.Queue (maxsize = 50)\n\t\tself.threadRunning = True\n\n\n\t# this method downloads a package from GitHub\n\t# it is called from run() below\n\t#\n\t# download requests are pushed for automatic downloads from mainloop\n\t# and also for a manual download triggered from the GUI\n\t#\n\t# automatic downloads that fail are logged but otherwise not reported\n\n\tdef GitHubDownload (self, packageName=None, source=None):\n\t\tif source == 'GUI':\n\t\t\twhere = 'Editor'\n\t\telif source == 'AUTO':\n\t\t\twhere = 'PmStatus'\n\t\telse:\n\t\t\twhere = None\n\n\t\terrorMessage = None\n\t\terrorDetails = None\n\t\tdownloadError = False\n\n\t\tif packageName == None or packageName == \"\":\n\t\t\tlogging.error (\"GitHubDownload: no package name specified\")\n\t\t\tdownloadError = True\n\n\t\tif not downloadError:\n\t\t\tpackagePath = \"/data/\" + packageName\n\t\t\ttempPackagePath = packagePath + \"-temp\"\n\n\t\t\tDbusIf.LOCK (\"GitHubDownload - get GitHub user/branch\")\n\t\t\tpackage = PackageClass.LocatePackage (packageName)\n\t\t\tgitHubUser = package.GitHubUser\n\t\t\tgitHubBranch = package.GitHubBranch\n\t\t\tDbusIf.UNLOCK (\"GitHubDownload - get GitHub user/branch\")\n\n\t\t\tDbusIf.UpdateStatus ( message=\"downloading \" + packageName, where=where, logLevel=INFO )\n\n\t\t\ttempDirectory = \"/data/PmDownloadTemp\"\n\t\t\tif not os.path.exists (tempDirectory):\n\t\t\t\tos.mkdir (tempDirectory)\n\n\t\t\t# create temp directory specific to this thread\n\t\t\ttempArchiveFile = tempDirectory + \"/temp.tar.gz\"\n\t\t\t# download archive\n\t\t\tif os.path.exists (tempArchiveFile):\n\t\t\t\tos.remove ( tempArchiveFile )\n\n\t\t\turl = \"https://github.com/\" + gitHubUser + \"/\" + packageName  + \"/archive/\" + gitHubBranch  + \".tar.gz\"\n\t\t\ttry:\n\t\t\t\tproc = subprocess.Popen ( ['wget', '--timeout=120', '-qO', tempArchiveFile, url ],\n\t\t\t\t\t\t\t\t\tbufsize=-1, stdout=subprocess.PIPE, stderr=subprocess.PIPE )\n\t\t\t\t_, stderr = proc.communicate()\n\t\t\t\tstderr = stderr.decode ().strip ()\n\t\t\t\treturnCode = proc.returncode\n\t\t\texcept:\n\t\t\t\terrorMessage = \"could not access archive on GitHub \" + packageName\n\t\t\t\tdownloadError = True\n\t\t\telse:\n\t\t\t\tif returnCode != 0:\n\t\t\t\t\terrorMessage = \"could not access \" + packageName + ' ' + gitHubUser + ' ' + gitHubBranch + \" on GitHub\"\n\t\t\t\t\terrorDetails = \"returnCode:\" + str (returnCode)\n\t\t\t\t\tif stderr != \"\":\n\t\t\t\t\t\terrorDetails +=  \" stderr:\" + stderr\n\t\t\t\t\tdownloadError = True\n\t\tif not downloadError:\n\t\t\ttry:\n\t\t\t\tproc = subprocess.Popen ( ['tar', '-xzf', tempArchiveFile, '-C', tempDirectory ],\n\t\t\t\t\t\t\t\tbufsize=-1, stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n\t\t\t\t_, stderr = proc.communicate ()\n\t\t\t\tstderr = stderr.decode ().strip ()\n\t\t\t\treturnCode = proc.returncode\n\t\t\texcept:\n\t\t\t\terrorMessage = \"could not unpack \" + packageName + ' ' + gitHubUser + ' ' + gitHubBranch\n\t\t\t\tdownloadError = True\n\t\t\telse:\n\t\t\t\tif returnCode != 0:\n\t\t\t\t\terrorMessage = \"unpack failed \" + packageName + ' ' + gitHubUser + ' ' + gitHubBranch\n\t\t\t\t\terrorDetails = \"stderr: \" + stderr\n\t\t\t\t\tdownloadError = True\n\n\t\tif not downloadError:\n\t\t\t# attempt to locate a directory that contains a version file\n\t\t\t# the first directory in the tree starting with tempDirectory is returned\n\t\t\tunpackedPath = LocatePackagePath (tempDirectory)\n\t\t\tif unpackedPath == None:\n\t\t\t\terrorMessage = \"no archive path for \" + packageName\n\t\t\t\tdownloadError = True\n\n\t\tif not downloadError:\n\t\t\t# move unpacked archive to package location\n\t\t\t# LOCK this section of code to prevent others\n\t\t\t#\tfrom accessing the directory while it's being updated\n\t\t\ttry:\n\t\t\t\tif os.path.exists (tempPackagePath):\n\t\t\t\t\tshutil.rmtree (tempPackagePath, ignore_errors=True)\t# like rm -rf\n\t\t\texcept:\n\t\t\t\tpass\n\n\t\t\tDbusIf.LOCK (\"GitHubDownload - move package\")\n\t\t\ttry:\n\t\t\t\tif os.path.exists (packagePath):\n\t\t\t\t\tos.rename (packagePath, tempPackagePath)\n\t\t\t\tshutil.move (unpackedPath, packagePath)\n\t\t\t\t\n\t\t\texcept:\n\t\t\t\terrorMessage = \"couldn't update \" + packageName\n\t\t\t\tdownloadError = True\n\t\t\tDbusIf.UNLOCK (\"GitHubDownload - move package\")\n\n\t\tDbusIf.LOCK (\"GitHubDownload - update status\")\n\t\tpackage = PackageClass.LocatePackage (packageName)\n\t\tif package != None:\n\t\t\tinstallAfter = package.InstallAfterDownload\t# save install after flag for later, then clear it\n\t\t\tpackage.InstallAfterDownload = False\n\t\t\tpackage.DownloadPending = False\n\t\t\tif not downloadError:\n\t\t\t\t# update basic flags then request install\n\t\t\t\tif installAfter:\n\t\t\t\t\tpackage.UpdateVersionsAndFlags ()\n\t\t\t\t\tlogging.info (\"install after download requested for \" + packageName)\n\t\t\t\t\tPushAction ( command='install' + ':' + packageName, source=source )\n\t\t\t\t# no install after, do full version/flag update\n\t\t\t\telse:\n\t\t\t\t\tpackage.UpdateVersionsAndFlags (doConflictChecks=True, doScriptPreChecks=True)\n\t\tDbusIf.UNLOCK (\"GitHubDownload - update status\")\n\n\t\t# report errors / success\n\t\tif errorMessage != None:\n\t\t\tlogging.error (errorMessage)\n\t\tif errorDetails != None:\n\t\t\tlogging.error (errorDetails)\n\t\tDbusIf.UpdateStatus ( message=errorMessage, where=where )\n\t\tif source == 'GUI':\n\t\t\tif errorMessage != None:\n\t\t\t\tDbusIf.AcknowledgeGuiEditAction ( 'ERROR' )\n\t\t\t# don't ack success if there's more to do\n\t\t\telif not installAfter:\n\t\t\t\tDbusIf.AcknowledgeGuiEditAction ( '' )\n\n\t\t# remove any remaining temp directories\n\t\tif os.path.exists (tempPackagePath):\n\t\t\tshutil.rmtree (tempPackagePath, ignore_errors=True)\t# like rm -rf\n\t\tif os.path.exists (tempDirectory):\n\t\t\tshutil.rmtree (tempDirectory, ignore_errors=True)\n\t# end GitHubDownload\n\n\n\t#\tDownloadVersionCheck\n\t#\n\t# compares versions to determine if a download is needed\n\t# returns: True if a download is needed, False otherwise\n\t# must be called with package list LOCKED !!\n\n\t\n\tdef DownloadVersionCheck (self, package):\n\t\tgitHubUser = package.GitHubUser\n\t\tgitHubBranch = package.GitHubBranch\n\t\tgitHubVersion = package.GitHubVersion\n\t\tpackageVersion = package.PackageVersion\n\n\t\t# versions not initialized yet - don't allow the download\n\t\tif gitHubVersion == None or gitHubVersion == \"\" or gitHubVersion[0] != 'v' or packageVersion == '?':\n\t\t\treturn False\n\n\t\tpackageVersionNumber = package.PackageVersionNumber\n\t\tgitHubVersionNumber = package.GitHubVersionNumber\n\n\t\t# if GitHubBranch is a version number, a download is needed if the versions differ\n\t\tif gitHubBranch[0] == 'v':\n\t\t\tif gitHubVersionNumber != packageVersionNumber:\n\t\t\t\treturn True\n\t\t\telse:\n\t\t\t\treturn False\n\t\t# otherwise the download is needed if the gitHubVersion is newer\n\t\telse:\n\t\t\tif gitHubVersionNumber > packageVersionNumber:\n\t\t\t\treturn True\n\t\t\telse:\n\t\t\t\treturn False\n\n\n\t#\tDownloadGitHub run (the thread)\n\t#\n\t# StopThread () is called to shut down the thread\n\n\tdef StopThread (self):\n\t\tself.threadRunning = False\n\t\tself.DownloadQueue.put ( ('STOP', ''), block=False )\n\n\t#\tDownloadGitHub run (the thread)\n\t#\n\t# downloads packages placed on its queue from\n\t#\tGUI requests\n\t#\ta background loop in mainLoop\n\t#\n\t# run () checks the threadRunning flag and returns if it is False,\n\t#\tessentially taking the thread off-line\n\t#\tthe main method should catch the tread with join ()\n\n\tdef run (self):\n\t\twhile self.threadRunning:\t# loop forever\n\t\t\t# process one GUI download request\n\t\t\t# if there was one, skip auto downloads until next pass\n\t\t\ttry:\n\t\t\t\tcommand = self.DownloadQueue.get () # block forever\n\t\t\texcept:\n\t\t\t\tlogging.error (\"pull from DownloadQueue queue failed\")\n\t\t\t\ttime.sleep (5.0)\n\t\t\t\tcontinue\n\t\t\tif command[0] == 'STOP' or self.threadRunning == False:\n\t\t\t\treturn\n\n\t\t\t# separate command, source tuple\n\t\t\t# and separate action and packageName\n\t\t\tif len (command) >= 2:\n\t\t\t\tparts = command[0].split (\":\")\n\t\t\t\tif len (parts) >= 2:\n\t\t\t\t\taction = parts[0].strip ()\n\t\t\t\t\tpackageName = parts[1].strip ()\n\t\t\t\telse:\n\t\t\t\t\tlogging.error (\"DownloadQueue - no action and/or package name - discarding\", command)\n\t\t\t\t\tcontinue\n\t\t\t\tsource = command[1]\n\t\t\telse:\n\t\t\t\tlogging.error (\"DownloadQueue - no command and/or source - discarding\", command)\n\t\t\t\tcontinue\n\n\t\t\t# invalid action for this queue\n\t\t\tif action != 'download':\n\t\t\t\tlogging.error (\"received invalid command from Install queue: \", command )\n\t\t\t\tcontinue\n\n\t\t\t# do the download here\n\t\t\tself.GitHubDownload (packageName=packageName, source=source )\n\t\t# end while True\n\t# end run\n# end DownloadGitHubPackagesClass\n\t\t\t\t\t\n\n#\tInstallPackagesClass\n#\tInstances:\n#\t\tInstallPackages (a separate thread)\n#\n#\tMethods:\n#\t\tInstallPackage\n#\t\tResolveConflicts\n#\t\trun (the thread)\n#\t\tStopThread\n#\t\trun\n#\n# runs as a separate thread since the operations can take a long time\n# \tand we need to space them to avoid consuming all CPU resources\n#\n# packages are automatically installed only\n#\tif the autoInstall Setting is active\n#\tpackage version is newer than installed version\n#\t\t\tor if nothing is installed\n#\n#\ta manual install is performed regardless of versions\n\nclass InstallPackagesClass (threading.Thread):\n\n\tdef __init__(self):\n\t\tthreading.Thread.__init__(self)\n\t\tDbusIf.SetPmStatus (\"\")\n\t\tself.threadRunning = True\n\t\tself.InstallQueue = queue.Queue (maxsize = 10)\n\n\t\n\t#\tInstallPackage\n\t#\n\t# this method either installs, uninstalls or checks a package\n\t#\tby calling the package's setup script\n\t#\n\t# the 'check' action runs file set checks without installing the package\n\t#\tthis creates a missing file set then sets/clears the INCOMPLETE flag\n\t#\ta missing file set is reported as \"no file set\" but does not block installs\n\t#\tan INCOMPLETE file set blocks installs\n\t#\ttherefore, check attempts to resolve a missing file set\n\t#\n\t# the operation can take many seconds\n\t# \ti.e., the time it takes to run the package's setup script\n\t#\n\t# uninstalling SetupHelper is a special case since that action will end PackageManager\n\t#\tso it is deferred until mainLoop detects the request and exits to main\n\t#\twhere the actual uninstall occurs\n\n\tdef InstallPackage ( self, packageName=None, source=None , action='install' ):\n\n\t\t# refresh versions, then check to see if an install is possible\n\t\tDbusIf.LOCK (\"InstallPackage\")\n\t\tpackage = PackageClass.LocatePackage (packageName)\n\n\t\tif package == None:\n\t\t\tlogging.error (\"InstallPackage: \" + packageName + \" not in package list\")\n\t\t\tif source == 'GUI':\n\t\t\t\tDbusIf.UpdateStatus ( message=packageName + \" not in package list\", where='Editor' )\n\t\t\t\tDbusIf.AcknowledgeGuiEditAction ( 'ERROR' )\n\t\t\tDbusIf.UNLOCK (\"InstallPackage error 1\")\n\t\t\treturn\n\n\t\tif source == 'GUI':\n\t\t\tsendStatusTo = 'Editor'\n\t\t\t# uninstall sets the uninstall flag file to prevent auto install\n\t\t\tif action == 'uninstall':\n\t\t\t\tpackage.SetAutoInstallOk (False)\n\t\t\t\tlogging.info (packageName + \" was manually uninstalled - auto install for that package will be skipped\")\n\t\t\t# manual install removes the flag file\n\t\t\telif action == 'install':\n\t\t\t\tpackage.SetAutoInstallOk (True)\n\t\t\t\tlogging.info (packageName + \" was manually installed - allowing auto install for that package\")\n\t\telif source == 'AUTO':\n\t\t\tsendStatusTo = 'PmStatus'\n\n\t\tpackageDir = \"/data/\" + packageName\n\t\tif not os.path.isdir (packageDir):\n\t\t\terrorMessage = \"no package directory \" + packageName\n\t\t\tlogging.error (\"InstallPackage - \" + errorMessage)\n\t\t\tpackage.InstallPending = False\n\t\t\tpackage.UpdateVersionsAndFlags ()\n\t\t\tif source == 'GUI':\n\t\t\t\tDbusIf.AcknowledgeGuiEditAction ( 'ERROR' )\n\t\t\tDbusIf.UNLOCK (\"InstallPackage error 2\")\n\t\t\treturn\n\t\t\t\n\t\tsetupFile = packageDir + \"/setup\"\n\t\tif not os.path.isfile(setupFile):\n\t\t\terrorMessage = \"setup file for \" + packageName + \" doesn't exist\"\n\t\t\tDbusIf.UpdateStatus ( message=errorMessage,\twhere=sendStatusTo, logLevel=ERROR )\n\t\t\tpackage.InstallPending = False\n\t\t\tpackage.UpdateVersionsAndFlags ()\n\t\t\tif source == 'GUI':\n\t\t\t\tDbusIf.AcknowledgeGuiEditAction ( 'ERROR' )\n\t\t\tDbusIf.UNLOCK (\"InstallPackage error 3\")\n\t\t\treturn\n\t\telif os.access(setupFile, os.X_OK) == False:\n\t\t\terrorMessage = \"setup file for \" + packageName + \" not executable\"\n\t\t\tDbusIf.UpdateStatus ( message=errorMessage,\twhere=sendStatusTo, logLevel=ERROR )\n\t\t\tpackage.InstallPending = False\n\t\t\tif source == 'GUI':\n\t\t\t\tDbusIf.AcknowledgeGuiEditAction ( 'ERROR' )\n\t\t\tDbusIf.UNLOCK (\"InstallPackage error 4\")\n\t\t\treturn\n\n\t\tDbusIf.UNLOCK (\"InstallPackage normal\")\n\n\t\tDbusIf.UpdateStatus ( message=action + \"ing \" + packageName, where=sendStatusTo, logLevel=INFO )\n\t\ttry:\n\t\t\tproc = subprocess.Popen ( [ setupFile, action, 'runFromPm' ],\n\t\t\t\t\t\t\t\t\t\tbufsize=100000, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True )\n\t\t\tproc.wait ()\n\t\t\t# forward stdout lines from setup script to console/log file\n\t\t\tfor line in proc.stdout:\n\t\t\t\tlogging.info ( line.strip () )\n\t\t\t# collect stderr lines for possible use later\n\t\t\tstderr = \"\"\n\t\t\tfor line in proc.stderr:\n\t\t\t\tstderr += line\n\t\t\treturnCode = proc.returncode\n\t\t\tsetupRunFail = False\n\t\texcept:\n\t\t\tsetupRunFail = True\n\n\t\t# manage the result of the setup run while locked just in case\n\t\tDbusIf.LOCK (\"InstallPackage - update status\")\n\n\t\tpackage = PackageClass.LocatePackage (packageName)\n\t\tpackage.InstallPending = False\n\n\t\terrorMessage = \"\"\n\t\tif setupRunFail:\n\t\t\terrorMessage = \"could not run setup\"\n\t\telif returnCode == EXIT_SUCCESS:\n\t\t\tDbusIf.UpdateStatus ( message=\"\", where=sendStatusTo )\n\t\t\tif source == 'GUI':\n\t\t\t\tDbusIf.AcknowledgeGuiEditAction ( '' )\n\t\telif returnCode == EXIT_REBOOT:\n\t\t\tpackage.ActionNeeded = REBOOT_NEEDED\n\t\t\tif source == 'GUI':\n\t\t\t\tlogging.info ( packageName + \" \" + action + \" REBOOT needed but handled by GUI\")\n\t\t\t\tDbusIf.UpdateStatus ( message=\"\", where=sendStatusTo )\n\t\t\t\tDbusIf.AcknowledgeGuiEditAction ( \"\" )\n\t\t\t# auto install triggers a reboot by setting the global flag - reboot handled in main_loop\n\t\t\telse:\n\t\t\t\tlogging.info ( packageName + \" \" + action + \" REBOOT pending\")\n\t\t\t\tglobal SystemReboot\n\t\t\t\tSystemReboot = True\n\t\telif returnCode == EXIT_RESTART_GUI:\n\t\t\tpackage.ActionNeeded = GUI_RESTART_NEEDED\n\t\t\tif source == 'GUI':\n\t\t\t\tlogging.info ( packageName + \" \" + action + \" GUI restart needed but handled by GUI\")\n\t\t\t\tDbusIf.UpdateStatus ( message=\"\", where=sendStatusTo )\n\t\t\t\tDbusIf.AcknowledgeGuiEditAction ( \"\" )\n\t\t\t# auto install triggers a GUI restart by setting the global flag - restart handled in main_loop\n\t\t\telse:\n\t\t\t\tlogging.info ( packageName + \" \" + action + \" GUI restart pending\")\n\t\t\t\tglobal GuiRestart\n\t\t\t\tGuiRestart = True\n\t\telif returnCode == EXIT_RUN_AGAIN:\n\t\t\tif source == 'GUI':\n\t\t\t\tDbusIf.UpdateStatus ( message=packageName + \" run install again to complete install\",\n\t\t\t\t\t\t\t\t\t\t\twhere=sendStatusTo, logLevel=INFO )\n\t\t\t\tDbusIf.AcknowledgeGuiEditAction ( 'ERROR' )\n\t\t\telse:\n\t\t\t\tDbusIf.UpdateStatus ( message=packageName + \" setup must be run again\",\n\t\t\t\t\t\t\t\t\t\t\twhere=sendStatusTo, logLevel=WARNING )\n\t\telif returnCode == EXIT_INCOMPATIBLE_VERSION:\n\t\t\tglobal VenusVersion\n\t\t\terrorMessage = \"incompatible with \" + VenusVersion\n\t\telif returnCode == EXIT_INCOMPATIBLE_PLATFORM:\n\t\t\tglobal Platform\n\t\t\terrorMessage = \"incompatible with \" + Platform\n\t\telif returnCode == EXIT_OPTIONS_NOT_SET:\n\t\t\terrorMessage = \"setup must be run from the command line\"\n\t\telif returnCode == EXIT_FILE_SET_ERROR:\n\t\t\terrorMessage = \"incomplete file set for \" + VenusVersion\n\n\t\telif returnCode == EXIT_ROOT_FULL:\n\t\t\terrorMessage = \"no room on root partition \"\n\t\telif returnCode == EXIT_DATA_FULL:\n\t\t\terrorMessage = \"no room on data partition \"\n\t\telif returnCode == EXIT_NO_GUI_V1:\n\t\t\terrorMessage = \"failed - \" + \"GUI v1 not installed\"\n\t\telif returnCode == EXIT_PACKAGE_CONFLICT:\n\t\t\terrorMessage = \"package conflict \" + stderr\n\t\telif returnCode == EXIT_PATCH_ERROR:\n\t\t\terrorMessage = \"could not patch some files\"\n\t\t# unknown error\n\t\telif returnCode != 0:\n\t\t\terrorMessage = \"unknown error \" + str (returnCode) + \" \" + stderr\n\n\t\tif errorMessage != \"\":\n\t\t\tif setupRunFail:\n\t\t\t\tlogLevel = ERROR\n\t\t\telse:\n\t\t\t\tlogLevel = INFO\n\t\t\tDbusIf.UpdateStatus ( message=packageName + \" \" + action + \" failed - \" + errorMessage,\n\t\t\t\t\twhere=sendStatusTo, logLevel=logLevel )\n\t\t\tif source == 'GUI':\n\t\t\t\tDbusIf.AcknowledgeGuiEditAction ( 'ERROR' )\n\n\t\t# installs do script conflict checks\n\t\t#\tupdate last check time here so checks aren't run right away\n\t\tpackage.lastScriptPrecheck = time.time ()\n\n\t\tpackage.UpdateVersionsAndFlags ()\n\n\t\tDbusIf.UNLOCK (\"InstallPackage - update status\")\n\t# end InstallPackage ()\n\n\n\t# \tResolveConflicts\n\t#\n\t# this method checks the conflicts for the indicated package\n\t# if conflicts exist, the conflicting package(s) are installed or uninstalled\n\t# by pushing them on the install queue\n\t\n\tdef ResolveConflicts ( self, packageName=None, source=None ):\n\n\t\tif packageName == None:\n\t\t\tlogging.error (\"ResolveConflicts - no package name specified\")\n\t\t\treturn\n\n\t\tDbusIf.LOCK (\"ResolveConflicts\")\n\t\n\t\tpackage = PackageClass.LocatePackage (packageName)\n\t\tif package == None:\n\t\t\tlogging.error (\"ResolveConflicts: \" + packageName + \"not found\")\n\n\t\tfor conflict in (package.DependencyErrors + package.FileConflicts):\n\t\t\tif len (conflict) < 2:\n\t\t\t\tlogging.error (\"ResolveConflicts: \" + packageName + \" missing parameters: \" + str (conflict) )\n\t\t\t\tcontinue\n\t\t\tdependencyPackage = conflict[0]\n\t\t\tdependencyRequirement = conflict[1]\n\t\t\tif dependencyRequirement == \"installed\":\n\t\t\t\tpackageMustBeInstalled = True\n\t\t\telif dependencyRequirement == \"uninstalled\":\n\t\t\t\tpackageMustBeInstalled = False\n\t\t\telse:\n\t\t\t\tlogging.error (\"ResolveConflicts: \" + packageName + \" unrecognized requirement: \" + str (conflict) )\n\t\t\t\tcontinue\n\n\t\t\trequiredPackage = PackageClass.LocatePackage (dependencyPackage)\n\n\t\t\tif requiredPackage.InstalledVersion != \"\":\n\t\t\t\tpackageIsInstalled = True\n\t\t\telse:\n\t\t\t\tpackageIsInstalled = False\n\t\t\tif requiredPackage.PackageVersion != \"\":\n\t\t\t\tpackageIsStored = True\n\t\t\telse:\n\t\t\t\tpackageIsStored = False\n\t\t\tif requiredPackage.GitHubVersion != \"\":\n\t\t\t\tpackageIsOnGitHub = True\n\t\t\telse:\n\t\t\t\tpackageIsOnGitHub = False\n\t\t\tif packageIsStored or packageIsOnGitHub:\n\t\t\t\tpackageIsAvailable = True\n\t\t\telse:\n\t\t\t\tpackageIsAvailable = True\n\n\t\t\tif packageMustBeInstalled and not packageIsInstalled:\n\t\t\t\tif not packageIsAvailable:\n\t\t\t\t\tDbusIf.UpdateStatus ( message=dependencyPackage + \" not available - can't install\",\n\t\t\t\t\t\t\t\twhere='Editor', logLevel=WARNING )\n\t\t\t\telif not packageIsStored and packageIsOnGitHub:\n\t\t\t\t\tlogging.info (\"ResolveConflicts: downloading and installing\" + dependencyPackage + \" so that \" + packageName + \" can be installed\" )\n\t\t\t\t\tPushAction ( command='download' + ':' + dependencyPackage, source=source )\n\t\t\t\t\t# download will trigger install when it finished\n\t\t\t\t\trequiredPackage.InstallAfterDownload = True\n\t\t\t\telse:\n\t\t\t\t\tlogging.info (\"ResolveConflicts: installing \" + dependencyPackage + \" so that \" + packageName + \" can be installed\" )\n\t\t\t\t\tPushAction ( command='install' + ':' + dependencyPackage, source=source )\n\n\t\t\telif not packageMustBeInstalled and packageIsInstalled:\n\t\t\t\tlogging.info (\"ResolveConflicts: uninstalling \" + dependencyPackage + \" so that \" + packageName + \" can be installed\" )\n\t\t\t\tPushAction ( command='uninstall' + ':' + dependencyPackage, source=source )\n\n\t\tDbusIf.UNLOCK (\"ResolveConflicts\")\n\n\n\t#\tInstallPackage run (the thread)\n\t#\n\t# automatic install packages\n\t#\tpushes request on queue for processing later in another thread\n\t#\t\tthis allows this to run quickly while the package list is locked\n\t#\n\t# run () checks the threadRunning flag and returns if it is False,\n\t#\tessentially taking the thread off-line\n\t#\tthe main method should catch the tread with join ()\n\t# StopThread () is called to shut down the thread\n\n\tdef StopThread (self):\n\t\tself.threadRunning = False\n\t\tself.InstallQueue.put ( ('STOP', ''), block=False )\n\n\tdef run (self):\n\t\twhile self.threadRunning:\n\t\t\ttry:\n\t\t\t\tcommand = self.InstallQueue.get ()\n\t\t\texcept:\n\t\t\t\tlogging.error (\"pull from Install queue failed\")\n\t\t\t\tcontinue\n\t\t\tif len (command) == 0:\n\t\t\t\tlogging.error (\"pull from Install queue failed - empty comand\")\n\t\t\t\tcontinue\n\t\t\t# thread shutting down\n\t\t\tif command[0] == 'STOP' or self.threadRunning == False:\n\t\t\t\treturn\n\n\t\t\t# separate command, source tuple\n\t\t\t# and separate action and packageName\n\t\t\tif len (command) >= 2:\n\t\t\t\tparts = command[0].split (\":\")\n\t\t\t\tif len (parts) >= 2:\n\t\t\t\t\taction = parts[0].strip ()\n\t\t\t\t\tpackageName = parts[1].strip ()\n\t\t\t\telse:\n\t\t\t\t\tlogging.error (\"InstallQueue - no action and/or package name - discarding\", command)\n\t\t\t\t\tcontinue\n\t\t\t\tsource = command[1]\n\t\t\telse:\n\t\t\t\tlogging.error (\"InstallQueue - no command and/or source - discarding\", command)\n\t\t\t\tcontinue\n\n\t\t\t# resolve conflicts may cause OTHER packages to install or uninstall\n\t\t\tif action == 'resolveConflicts':\n\t\t\t\tself.ResolveConflicts (packageName=packageName, source=source)\n\t\t\t# otherwise use InstallPackage to install, uninstall, or check the package\n\t\t\telif action == 'install' or action == 'uninstall' or action == 'check':\n\t\t\t\tself.InstallPackage (packageName=packageName, source=source , action=action )\n\n\t\t\t# invalid action for this queue\n\t\t\telse:\n\t\t\t\tlogging.error (\"received invalid command from Install queue: \", command )\n\t\t\t\tcontinue\n\t# end run\n# end InstallPackagesClass\n\n\n\n#\tMediaScanClass\n#\tInstances:\n#\t\tMediaScan (a separate thread)\n#\tMethods:\n#\t\ttransferPackage\n#\t\tStopThread\n#\t\tsettingsBackup\n#\t\tsettingsRestore\n#\t\trun\n#\n#\tscan removable SD and USB media for packages to be installed\n#\n#\trun () is a separate thread that looks for removable\n#\tSD cards and USB sticks that appear in /media as separate directories\n#\tthese directories come and go with the insertion and removable of the media\n#\n#\twhen new media is detected, it is scanned once then ignored\n#\twhen media is removed, then reinserted, the scan begins again\n#\n#\tpackages must be located in the root of the media (no subdirecoties are scanned)\n#\tand must be an archive with a name ending in .tar.gz\n#\n#\tarchives are unpacked to a temp directory in /var/run (a ram disk)\n#\tverified, then moved into position in /data/<packageName>\n#\twhere the name comes from the unpacked directory name\n#\tof the form <packageName>-<branch or version>\n#\n#\tactual installation is handled in the InstallPackages run() thread\n#\n# removable media is checked for several \"flag files\" that trigger actions elsewhere\n#\tthese are described in detail at the beginning of this file\n#\tscans for flag files is done in run ()\n\nclass MediaScanClass (threading.Thread):\n\n\n\t# transferPackage unpacks the archive and moves it into postion in /data\n\t#\n\t#\tpath is the full path to the archive\n\t#\n\t#\tif true, autoInstallOverride causes the ONE_TIME_INSTALL flag to be set\n\t#\t\tthis happens if the caller detects the AUTO_INSTALL_PACKAGES flag on removable media\n\n\tdef transferPackage (self, path, autoInstallOverride=False):\n\t\tpackageName = os.path.basename (path).split ('-', 1)[0]\n\n\t\t# create an empty temp directory in ram disk\n\t\t#\tfor the following operations\n\t\t# directory is unique to this process and thread\n\t\ttempDirectory = \"/var/run/packageManager\" + str(os.getpid ()) + \"Media\"\n\t\tif os.path.exists (tempDirectory):\n\t\t\tshutil.rmtree (tempDirectory)\n\t\tos.mkdir (tempDirectory)\n\n\t\t# unpack the archive - result is placed in tempDirectory\n\t\ttry:\n\t\t\tproc = subprocess.Popen ( ['tar', '-xzf', path, '-C', tempDirectory ],\n\t\t\t\t\t\t\tbufsize=-1, stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n\t\t\t_, stderr = proc.communicate ()\n\t\t\tstderr = stderr.decode ().strip ()\n\t\t\treturnCode = proc.returncode\n\t\texcept:\n\t\t\tDbusIf.UpdateStatus ( message=\"tar failed for \" + packageName,\n\t\t\t\t\t\t\t\t\twhere='Media', logLevel=ERROR)\n\t\t\ttime.sleep (5.0)\n\t\t\tDbusIf.UpdateStatus ( message=\"\", where='Media')\n\t\t\treturn False\n\t\tif returnCode != 0:\n\t\t\tDbusIf.UpdateStatus ( message=\"could not unpack \" + packageName + \" from SD/USB media\",\n\t\t\t\t\t\t\t\t\twhere='Media', logLevel=ERROR)\n\t\t\tlogging.error (\"stderr: \" + stderr)\n\t\t\tshutil.rmtree (tempDirectory)\n\t\t\ttime.sleep (5.0)\n\t\t\tDbusIf.UpdateStatus ( message=\"\", where='Media')\n\t\t\treturn False\n\n\t\t# attempt to locate a package directory in the tree below tempDirectory\n\t\tunpackedPath = LocatePackagePath (tempDirectory)\n\n\t\tif unpackedPath == None:\n\t\t\tlogging.warning (packageName + \" archive doesn't contain a package directory - rejected\" )\n\t\t\tshutil.rmtree (tempDirectory)\n\t\t\ttime.sleep (5.0)\n\t\t\tDbusIf.UpdateStatus ( message=\"\", where='Media')\n\t\t\treturn False\n\n\t\t# compare versions and proceed only if they are different\n\t\tpackagePath = \"/data/\" + packageName\n\t\ttry:\n\t\t\tfd = open (packagePath + \"/version\", 'r')\n\t\texcept:\n\t\t\tpackageVersion = 0\n\t\telse:\n\t\t\tpackageVersion = VersionToNumber (fd.readline().strip())\n\t\t\tfd.close ()\n\t\ttry:\n\t\t\tfd = open (unpackedPath + \"/version\", 'r')\n\t\texcept:\n\t\t\tunpackedVersion = 0\n\t\telse:\n\t\t\tunpackedVersion = VersionToNumber (fd.readline().strip())\n\t\t\tfd.close ()\n\t\tif packageVersion == unpackedVersion:\n\t\t\tlogging.info (\"transferPackages: \" + packageName + \" versions are the same - skipping transfer\")\n\t\t\tshutil.rmtree (tempDirectory)\n\t\t\tDbusIf.UpdateStatus ( message=\"\", where='Media')\n\t\t\treturn False\n\n\t\t# move unpacked archive to package location\n\t\t# LOCK this critical section of code to prevent others\n\t\t#\tfrom accessing the directory while it's being updated\n\t\tDbusIf.UpdateStatus ( message=\"transfering \" + packageName + \" from SD/USB\", where='Media', logLevel=INFO )\n\t\ttempPackagePath = packagePath + \"-temp\"\n\t\tDbusIf.LOCK (\"transferPackage\") \n\t\tif os.path.exists (tempPackagePath):\n\t\t\tshutil.rmtree (tempPackagePath, ignore_errors=True)\t# like rm -rf\n\t\tif os.path.exists (packagePath):\n\t\t\tos.rename (packagePath, tempPackagePath)\n\t\ttry:\n\t\t\tshutil.move (unpackedPath, packagePath)\n\t\texcept:\n\t\t\tlogging.error ( \"transferPackages: couldn't relocate \" + packageName )\n\t\tif os.path.exists (tempPackagePath):\n\t\t\tshutil.rmtree (tempPackagePath, ignore_errors=True)\t# like rm -rf\n\t\t# set package one-time install flag so this package is installed regardless of other flags\n\t\t#\tthis flag is removed when the install is preformed\n\t\tif autoInstallOverride:\n\t\t\tlogging.warning (\"Auto Install - setting ONE_TIME_INSTALL for \" + packageName )\n\t\t\topen ( packagePath + \"/ONE_TIME_INSTALL\", 'a').close()\n\n\t\tDbusIf.UNLOCK (\"transferPackage\")\n\t\tshutil.rmtree (tempDirectory, ignore_errors=True)\n\t\ttime.sleep (5.0)\n\t\tDbusIf.UpdateStatus ( message=\"\", where='Media')\n\t\treturn True\n\t# end transferPackage\n\n\n\tdef __init__(self):\n\t\tthreading.Thread.__init__(self)\n\t\tself.MediaQueue = queue.Queue (maxsize = 10) # used only for STOP\n\t\tself.threadRunning = True\n\t\tself.AutoUninstall = False\n\n\t#\n\t#\tsettingsBackup\n\t#\tsettingsRestore\n\t#\n\t# extracts / restores dbus settings and custom icons\n\t# copies ALL log files (backup only obvously)\n\t#\tthe files are zipped to save space\n\t#\n\t# backup and restore options are either to/from removable media or /data\n\t#\n\t# settingsList contains the list of dbus /Settings parameters to save and restore\n\t#\n\n\tdef settingsBackup (self, backupPath, settingsOnly = False):\n\t\tsettingsCount = 0\n\t\toverlayCount = 0\n\t\tlogsWritten = \"no logs\"\n\n\t\tsettingsListFile = \"/data/SetupHelper/settingsList\"\n\t\tbackupFile = backupPath + \"/settingsBackup\"\n\t\ttry:\n\t\t\tif not os.path.exists (settingsListFile):\n\t\t\t\tlogging.error (settingsListFile + \" does not exist - can't backup settings\")\n\t\t\t\treturn\n\n\t\t\t# backup settings\n\t\t\tbackupSettings = open (backupFile, 'w')\n\t\t\tbus = dbus.SystemBus()\n\t\t\twith open (settingsListFile, 'r') as listFile:\n\t\t\t\tfor line in listFile:\n\t\t\t\t\tsetting = line.strip()\n\t\t\t\t\ttry:\n\t\t\t\t\t\tvalue =  bus.get_object(\"com.victronenergy.settings\", setting).GetValue()\n\t\t\t\t\t\tattributes = bus.get_object(\"com.victronenergy.settings\", setting).GetAttributes()\n\t\t\t\t\texcept:\n\t\t\t\t\t\tcontinue\n\t\t\t\t\tdataType = type (value)\n\t\t\t\t\tif dataType is dbus.Double:\n\t\t\t\t\t\ttypeId = 'f'\n\t\t\t\t\telif dataType is dbus.Int32 or dataType is dbus.Int64:\n\t\t\t\t\t\ttypeId = 'i'\n\t\t\t\t\telif dataType is dbus.String:\n\t\t\t\t\t\ttypeId = 's'\n\t\t\t\t\telse:\n\t\t\t\t\t\ttypeId = ''\n\t\t\t\t\t\tlogging.error (\"settingsBackup - invalid data type \" + typeId + \" - can't include parameter attributes \" + setting)\n\n\t\t\t\t\tvalue = str ( value )\n\t\t\t\t\tdefault = str (attributes[0])\n\t\t\t\t\tmin = str (attributes[1])\n\t\t\t\t\tmax = str (attributes[2])\n\t\t\t\t\tsilent = str (attributes[3])\n\t\t\t\t\t\n\t\t\t\t\t# create entry with just settng path and value without a valid data type\n\t\t\t\t\tif typeId == '':\n\t\t\t\t\t\tline = ','.join ( [ setting, value ]) + '\\n'\n\t\t\t\t\telse:\n\t\t\t\t\t\tline = ','.join ( [ setting, value, typeId, default, min, max, silent ]) + '\\n'\n\n\t\t\t\t\tbackupSettings.write (line)\n\t\t\t\t\tsettingsCount += 1\n\n\t\t\tbackupSettings.close ()\n\t\t\tlistFile.close ()\n\t\texcept:\n\t\t\tlogging.error (\"settings backup - settings write failure\")\n\t\t\n\t\tif not settingsOnly:\n\t\t\t# backup logo overlays\n\t\t\toverlaySourceDir = \"/data/themes/overlay\"\n\t\t\toverlayDestDir = backupPath + \"/logoBackup\"\n\n\t\t\t\n\t\t\t# remove any previous logo backups\n\t\t\tif os.path.isdir (overlayDestDir):\n\t\t\t\tshutil.rmtree (overlayDestDir)\n\n\t\t\ttry:\n\t\t\t\tif os.path.isdir (overlaySourceDir):\n\t\t\t\t\toverlayFiles = os.listdir (overlaySourceDir)\n\t\t\t\t\tif len (overlayFiles) > 0:\n\t\t\t\t\t\t# create overlay direcory on backkup device, then copy files\n\t\t\t\t\t\tif not os.path.isdir (overlayDestDir):\n\t\t\t\t\t\t\tos.mkdir (overlayDestDir)\n\t\t\t\t\t\tfor overlay in overlayFiles:\n\t\t\t\t\t\t\tif overlay[0] == \".\":\n\t\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t\tshutil.copy ( overlaySourceDir + \"/\" + overlay, overlayDestDir )\n\t\t\t\t\t\t\toverlayCount += 1\n\t\t\texcept:\n\t\t\t\tlogging.error (\"settings backup - logo write failure\")\n\n\t\t\t# copy log files\n\t\t\ttry:\n\t\t\t\t# remove any previous log backups\n\t\t\t\tlogDestDir = backupPath + \"/logs\"\n\t\t\t\tif os.path.isdir (logDestDir):\n\t\t\t\t\tshutil.rmtree (logDestDir)\n\n\t\t\t\tproc = subprocess.Popen ( [ 'zip', '-rq', backupPath + \"/logs.zip\", \"/data/log\" ],\n\t\t\t\t\t\t\t\t\t\tbufsize=-1, stdout=subprocess.PIPE, stderr=subprocess.PIPE )\n\t\t\t\tproc.commiunicate()\t#output ignored\n\t\t\t\treturnCode = proc.returncode\n\t\t\t\tlogsWritten = \"logs\"\n\t\t\texcept:\n\t\t\t\tlogging.error (\"settings backup - log write failure\")\n\t\t\t\tlogsWritten = \"no logs\"\n\n\n\n\t\t\t# backup setup script options\n\t\t\toptionsSourceDir = \"/data/setupOptions\"\n\t\t\toptionsDestDir = backupPath + \"/setupOptions\"\n\n\t\t\ttry:\n\t\t\t\t# remove any previous options backups\n\t\t\t\tif os.path.isdir (optionsDestDir):\n\t\t\t\t\tshutil.rmtree (optionsDestDir)\n\n\t\t\t\tif os.path.isdir (optionsSourceDir):\n\t\t\t\t\tshutil.copytree ( optionsSourceDir, optionsDestDir )\n\t\t\texcept:\n\t\t\t\tlogging.error (\"settings backup - overlays write failure\")\n\t\t\n\t\tlogging.info (\"settings backup completed - \" + str(settingsCount) + \" settings, \" + str (overlayCount) + \" logos, \"\n\t\t\t\t\t\t\t+ logsWritten )\n\n\n\tdef settingsRestore (self, backupPath, settingsOnly = False):\n\t\tbackupFile = backupPath + \"/settingsBackup\"\n\t\tif not os.path.exists (backupFile):\n\t\t\tlogging.error (backupFile + \" does not exist - can't restore settings\")\n\t\tbus = dbus.SystemBus()\n\t\tsettingsCount = 0\n\t\toverlayCount = 0\n\n\n\t\twith open (backupFile, 'r') as fd:\n\t\t\tfor line in fd:\n\t\t\t\tparameterExists = False\n\t\t\t\t# ( setting path, value, attributes)\n\t\t\t\tparts = line.strip().split (',')\n\t\t\t\tnumberOfParts = len (parts)\n\t\t\t\t# full entry with attributes\n\t\t\t\tif numberOfParts == 7:\n\t\t\t\t\ttypeId = parts[2]\n\t\t\t\t\tdefault = parts[3]\n\t\t\t\t\tmin = parts[4]\n\t\t\t\t\tmax = parts[5]\n\t\t\t\t\tsilent = parts[6]\n\t\t\t\t# only path and name - old settings file format\n\t\t\t\telif numberOfParts == 2:\n\t\t\t\t\ttypeId = ''\n\t\t\t\t\tdefault = ''\n\t\t\t\t\tmin = ''\n\t\t\t\t\tmax = ''\n\t\t\t\t\tsilent = ''\n\t\t\t\telse:\n\t\t\t\t\tlogging.error (\"settingsRestore: invalid line in file \" + line)\n\t\t\t\t\tcontinue\n\t\t\t\t\n\t\t\t\tpath = parts[0]\n\t\t\t\tvalue = parts[1]\n\t\t\t\ttry:\n\t\t\t\t\tbus.get_object(\"com.victronenergy.settings\", path).GetValue()\n\t\t\t\t\tparameterExists = True\n\t\t\t\texcept:\n\t\t\t\t\tpass\n\n\t\t\t\tif not parameterExists:\n\t\t\t\t\tif typeId == '':\n\t\t\t\t\t\tlogging.error (\"settingsRestore: no attributes in settingsBackup file - can't create \" + path)\n\t\t\t\t\t# parameter does not yet exist, create it\n\t\t\t\t\telse:\n\t\t\t\t\t\t# silent uses a different method\n\t\t\t\t\t\tif silent == 1:\n\t\t\t\t\t\t\tmethod = 'AddSettingSilent'\n\t\t\t\t\t\telse:\n\t\t\t\t\t\t\tmethod = 'AddSetting'\n\n\t\t\t\t\t\ttry:\n\t\t\t\t\t\t\tproc = subprocess.Popen ( [ 'dbus', '-y', 'com.victronenergy.settings', '/', method, '',\n\t\t\t\t\t\t\t\t\t\t\tpath, default, typeId, min, max ], \n\t\t\t\t\t\t\t\t\t\t\tbufsize=-1, stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n\t\t\t\t\t\t\tproc.commiunicate ()\t# output ignored\n\t\t\t\t\t\t\tparameterExists = True\n\t\t\t\t\t\t\tlogging.info (\"settingsRestore: creating \" + path)\n\t\t\t\t\t\texcept:\n\t\t\t\t\t\t\tlogging.error (\"settingsRestore: settings create failed for \" + path)\n\n\t\t\t\t# update parameter's value if it exists (or was just created)\n\t\t\t\tif parameterExists:\n\t\t\t\t\tbus.get_object(\"com.victronenergy.settings\", path).SetValue (value)\n\t\t\t\t\tsettingsCount += 1\n\n\t\tif not settingsOnly:\n\t\t\t# restore logo overlays\n\t\t\toverlaySourceDir = backupPath + \"/logoBackup\"\n\t\t\toverlayDestDir = \"/data/themes/overlay\"\n\t\t\tif os.path.isdir (overlaySourceDir):\n\t\t\t\toverlayFiles = os.listdir (overlaySourceDir)\n\t\t\t\tif len (overlayFiles) > 0:\n\t\t\t\t\t# create overlay direcory in /data, then copy files\n\t\t\t\t\tif not os.path.isdir (overlayDestDir):\n\t\t\t\t\t\tos.mkdir (overlayDestDir)\n\t\t\t\t\tfor overlay in overlayFiles:\n\t\t\t\t\t\tif overlay[0] == \".\":\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\ttry:\n\t\t\t\t\t\t\tshutil.copy ( overlaySourceDir + \"/\" + overlay, overlayDestDir )\n\t\t\t\t\t\texcept:\n\t\t\t\t\t\t\tlogging.error (\"settingsRestore: overlay create failed for \" + overlay)\n\t\t\t\t\t\toverlayCount += 1\n\n\t\t\t# restore setup script options\n\t\t\toptionsSourceDir = backupPath + \"/setupOptions\"\n\t\t\toptionsDestDir = \"/data/setupOptions\"\n\n\t\t\t# remove any previous options backups\n\t\t\tif os.path.isdir (optionsDestDir):\n\t\t\t\tshutil.rmtree (optionsDestDir)\n\n\t\t\tif os.path.isdir (optionsSourceDir):\n\t\t\t\ttry:\n\t\t\t\t\tshutil.copytree ( optionsSourceDir, optionsDestDir )\n\t\t\t\texcept:\n\t\t\t\t\tlogging.error (\"settingsRestore: options restore failed\")\n\t\t\n\t\tlogging.info (\"settings restore completed - \" + str(settingsCount) + \" settings and \" + str (overlayCount) + \" overlays\")\n\n\n\t#\tMedia Scan run (the thread)\n\t#\n\t# run () checks the threadRunning flag and returns if it is False,\n\t#\tessentially taking the thread off-line\n\t#\tthe main method should catch the tread with join ()\n\t# StopThread () is called to shut down the thread\n\t#\n\t# a queue with set timeout is used to pace operations\n\t#\t this gives other threads time away from slower media scanning operations\n\n\tdef StopThread (self):\n\t\tself.threadRunning = False\n\t\tself.MediaQueue.put  ( \"STOP\", block=False )\n\n\tdef run (self):\n\t\tseparator = '/'\n\t\troot = \"/media\"\n\t\tarchiveSuffix = \".tar.gz\"\n\t\tautoRestore = False\n\t\tautoRestoreComplete = False\n\t\tautoEject = False\n\t\tbus = dbus.SystemBus()\n\t\tlocalSettingsBackupExists = True\n\n\t\t# list of accepted branch/version substrings\n\t\tacceptList = [ \"-current\", \"-latest\", \"-main\", \"-test\", \"-debug\", \"-beta\", \"-install\", \n\t\t\t\t\t\t\t\"-0\", \"-1\", \"-2\", \"-3\", \"-4\", \"-5\", \"-6\", \"-7\", \"-8\", \"-9\" ]\n\n\t\t# keep track of all media that's been scanned so it isn't scanned again\n\t\t# media removal removes it from this list\n\t\talreadyScanned = []\n\t\twhile self.threadRunning:\n\t\t\t# use queue to receive stop command and also to space operations\n\t\t\tcommand = \"\"\n\t\t\ttry:\n\t\t\t\tcommand = self.MediaQueue.get (timeout = 5.0)\n\t\t\texcept queue.Empty:\t# queue empty is OK\n\t\t\t\t# timeout indicates it's time to make one pass through the code below\n\t\t\t\tpass\n\t\t\texcept:\n\t\t\t\tlogging.error (\"pull from MediaQueue failed\")\n\t\t\t\ttime.sleep (5.0)\n\t\t\tif command == 'STOP' or self.threadRunning == False:\n\t\t\t\treturn\n\n\t\t\t# automaticTransfers is used to signal when anything is AUTOMATICALLY\n\t\t\t# transferred from or to removable media\n\t\t\t# this includes:\n\t\t\t#\ttransfrring a package from removable media to /data\n\t\t\t#\tperforming an automatic settings restore\n\t\t\t# Manually triggered operations do not update these operations\n\t\t\t#\tmanually triggered settings backup\n\t\t\t#\tmanually triggered settings restore\n\t\t\tautomaticTransfers = False\n\n\t\t\t# do local settings backup/restore\n\t\t\tif os.path.exists (\"/data/settingsBackup\"):\n\t\t\t\tlocalSettingsBackupExists = True\n\t\t\t\tDbusIf.SetBackupSettingsLocalFileExist (True)\n\t\t\telse:\n\t\t\t\tlocalSettingsBackupExists = False\n\t\t\t\tDbusIf.SetBackupSettingsLocalFileExist (False)\n\n\t\t\tbackupProgress = DbusIf.GetBackupProgress ()\n\t\t\tif backupProgress == 21:\n\t\t\t\tDbusIf.SetBackupProgress (23)\n\t\t\t\tself.settingsBackup (\"/data\", settingsOnly = True)\n\t\t\t\tDbusIf.SetBackupProgress (0)\n\t\t\telif backupProgress == 22:\n\t\t\t\tif localSettingsBackupExists:\n\t\t\t\t\tDbusIf.SetBackupProgress (24)\n\t\t\t\t\tself.settingsRestore (\"/data\", settingsOnly = True)\n\t\t\t\tDbusIf.SetBackupProgress (0)\n\n\n\t\t\ttry:\n\t\t\t\tdrives = os.listdir (root)\n\t\t\texcept:\n\t\t\t\tdrives = []\n\n\t\t\tif len (drives) == 0:\n\t\t\t\tDbusIf.SetBackupMediaAvailable (False)\n\t\t\t\tDbusIf.SetBackupSettingsFileExist (False)\n\t\t\t\tbackupMediaExists = False\n\t\t\telse:\n\t\t\t\tDbusIf.SetBackupMediaAvailable (True)\n\t\t\t\tbackupMediaExists = True\n\n\t\t\t# if previously detected media is removed,\n\t\t\t#\tallow it to be scanned again when reinserted\n\t\t\tfor scannedDrive in alreadyScanned:\n\t\t\t\tif not scannedDrive in drives:\n\t\t\t\t\talreadyScanned.remove (scannedDrive)\n\n\t\t\tfor drive in drives:\n\t\t\t\tdrivePath = separator.join ( [ root, drive ] )\n\t\t\t\tself.AutoUninstall = False\n\n\t\t\t\t# process settings backup and restore\n\t\t\t\t# check for settings backup file\n\t\t\t\tmediaSettingsBackupPath = root + \"/\" + drive\n\t\t\t\tif os.path.exists (mediaSettingsBackupPath + \"/settingsBackup\"):\n\t\t\t\t\tDbusIf.SetBackupSettingsFileExist (True)\n\t\t\t\t\tbackupSettingsFileExists = True\n\t\t\t\telse:\n\t\t\t\t\tDbusIf.SetBackupSettingsFileExist (False)\n\t\t\t\t\tbackupSettingsFileExists = False\n\n\t\t\t\tif backupMediaExists:\n\t\t\t\t\tautoRestoreFile = mediaSettingsBackupPath + \"/SETTINGS_AUTO_RESTORE\"\n\t\t\t\t\tif os.path.exists (autoRestoreFile):\n\t\t\t\t\t\tautoRestore = True\n\n\t\t\t\t\tautoEjectFile = mediaSettingsBackupPath + \"/AUTO_EJECT\"\n\t\t\t\t\tif os.path.exists (autoEjectFile):\n\t\t\t\t\t\tautoEject = True\n\n\t\t\t\t\tinitializeFile = mediaSettingsBackupPath + \"/INITIALIZE_PACKAGE_MANAGER\"\n\t\t\t\t\tif os.path.exists (initializeFile):\n\t\t\t\t\t\tglobal InitializePackageManager\n\t\t\t\t\t\tInitializePackageManager = True\n\n\t\t\t\t\t\n\t\t\t\t\t# set the auto install flag for use elsewhere\n\t\t\t\t\tautoInstallOverride = False\n\t\t\t\t\tautoUnInstallFile = mediaSettingsBackupPath + \"/AUTO_UNINSTALL_PACKAGES\"\n\t\t\t\t\tif os.path.exists (autoUnInstallFile):\n\t\t\t\t\t\tself.AutoUninstall = True\n\n\t\t\t\t\t# set the auto install flag for use elsewhere\n\t\t\t\t\t# auto Uninstall overrides auto install\n\t\t\t\t\tif not self.AutoUninstall:\n\t\t\t\t\t\t# check for auto install on media\n\t\t\t\t\t\tautoInstallFile = mediaSettingsBackupPath + \"/AUTO_INSTALL_PACKAGES\"\n\t\t\t\t\t\tif os.path.exists (autoInstallFile):\n\t\t\t\t\t\t\tautoInstallOverride = True \t\n\t\t\t\t\t\n\t\t\t\t\tbackupProgress = DbusIf.GetBackupProgress ()\n\t\t\t\t\t# GUI triggered backup\n\t\t\t\t\tif backupProgress == 1:\n\t\t\t\t\t\tDbusIf.SetBackupProgress (3)\n\t\t\t\t\t\tself.settingsBackup (mediaSettingsBackupPath)\n\t\t\t\t\t\tDbusIf.SetBackupProgress (0)\n\t\t\t\t\telif backupProgress == 2 or ( autoRestore and not autoRestoreComplete ):\n\t\t\t\t\t\tif backupSettingsFileExists:\n\t\t\t\t\t\t\tDbusIf.SetBackupProgress (4)\n\t\t\t\t\t\t\tself.settingsRestore (mediaSettingsBackupPath)\n\t\t\t\t\t\t\tif autoRestore:\n\t\t\t\t\t\t\t\tautoRestoreComplete = True\n\t\t\t\t\t\t\t\tautomaticTransfers = True\n\t\t\t\t\t\tDbusIf.SetBackupProgress (0)\n\n\t\t\t\t# if we've scanned this drive previously, it won't have any new packages to transfer\n\t\t\t\t# \tso skip it to avoid doing it again\n\t\t\t\tif drive not in alreadyScanned:\n\t\t\t\t\t# check any file name ending with the achive suffix\n\t\t\t\t\t#\tall others are skipped\n\t\t\t\t\tfor path in glob.iglob (drivePath + \"/*\" + archiveSuffix):\n\t\t\t\t\t\taccepted = False\n\t\t\t\t\t\tif os.path.isdir (path):\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\telse:\n\t\t\t\t\t\t\taccepted = False\n\t\t\t\t\t\t\tbaseName = os.path.basename (path)\n\t\t\t\t\t\t\t# verify the file name contains one of the accepted branch/version identifiers\n\t\t\t\t\t\t\t#\tif not found in the list, the archive is rejected\n\t\t\t\t\t\t\tfor accept in acceptList:\n\t\t\t\t\t\t\t\tif accept in baseName:\n\t\t\t\t\t\t\t\t\taccepted = True\n\t\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t\t# discovered what appears to be a valid archive\n\t\t\t\t\t\t\t# unpack it, do further tests and move it to /data \n\t\t\t\t\t\t\tif accepted:\n\t\t\t\t\t\t\t\tif self.transferPackage (path, autoInstallOverride):\n\t\t\t\t\t\t\t\t\tautomaticTransfers = True\n\t\t\t\t\t\t\t\tif self.threadRunning == False:\n\t\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\telse:\n\t\t\t\t\t\t\t\tlogging.warning (path + \" not a valid archive name - rejected\")\n\t\t\t\t\t# mark this drive so it won't get scanned again\n\t\t\t\t\t#\tthis prevents repeated installs\n\t\t\t\t\talreadyScanned.append (drive)\n\t\t\t\t\t# end if drive not in alreadyScanned\n\t\t\t\t# end for path\n\t\t\t#end for drive\n\n\t\t\t# we have arrived at a point where all removable media has been scanned\n\t\t\t# and all possible work has been done\n\n\t\t\t# eject removable media if work has been done and the\n\t\t\t# the AUTO_EJECT flag file was fouund on removable media during the most recent scan\n\t\t\t# NOTE: this ejects ALL removable media whether or not they are involved in transfers\n\t\t\tif automaticTransfers and autoEject:\n\t\t\t\tlogging.warning (\"automatic media transfers have occured, ejecting ALL removable media\")\n\t\t\t\tbus.get_object(\"com.victronenergy.logger\", \"/Storage/MountState\").SetValue (2)\n\n\t\t\tif not backupMediaExists:\n\t\t\t\tautoRestore = False\n\t\t\t\tautoEject = False\n\t\t\t\tautoRestoreComplete = False\n\n\t\t# end while\n\t# end run ()\n# end MediaScanClass\n\n\n\n# main and mainLoop\n#\n#\tMethods:\n#\t\tmainLoop\n#\t\tmain ( the entry point)\n#\t\tdirectUninstall\n#\n#\tmainLoop is called each second to make background checks\n#\tupdate global and package flags, versions, etc\n#\tand schedule automatic installs, uninstalls, downloads, etc\n#\n#\toperations that can not be done in line may be deferred here:\n#\t\tGUI restarts and system reboots\n#\t\tPackageManager restarts and INITIALIZE\n#\t\tGUI command acknowledgement from within the thread of the command handler\n#\n#\thandshakes between threads often use a global variable rather than pushing something on a queue:\n#\t\tinstall/download check holdoff while waiting for GitHub version refresh\n#\n#\tsome operations performed in mainLoop take a while but do not need to be done every second\n#\tto spread things out, and minimize impact on other tasks within Venus Os,\n#\tonly one package is checked each second\n# \t(it would take 10 seconds to scan 10 packages)\n#\n#\tPackageManager is responsible for reinstalling packages following a firmware update\n#\treinstallMods is a script called from /data/rcS.local that installs the PackageManager service\n#\t\tthen sets the /etc/venus/REINSTALL_PACKAGES flag file instructing PackageManager\n#\t\tto do a boot-time check all packages for possible reinstall\n#\t\tPackageManager clears that flag when all packages have been reinstalled\n#\tboot-time reinstall is done using the normal automatic install mechanism but bypasses\n#\t\tthe test for the user selectable auto install on/off\n#\n#\tmainLoop is resonsible for triggering GUI restarts and system reboots\n#\tinstalls, uninstalls and downloads are handled by the package's setup script\n#\t\tbut the script does not initalte these options.\n#\tthe results of the setup script set reboot/reset flags for the package\n#\tthe actual restart/reboot actions are held off as long as any package operations are pending\n#\t\tso repeaded GUI restarts or system reboots are avoided\n#\tthis gives all packages a chance to download and/or install automatically\n#\t\tbefore the restart/reboot\n#\n#\tmainLoop also handles PackageManager restart and INITIALIZE actions\n#\tINITIALZE wipes out the package information in dbus /Settings then quits\n#\tThe package information is then rebuilt the next time PackageManager starts\n#\n#\tmainLoop also provides status information to the GUI\n#\n#\tGUI restarts are handled without exiting PackageManager, however\n#\tsystem reboots and Package manager restarts and INITIALIZE actions require\n#\tPackageManager to exit\n#\tThe actual operations are performed in main () after mainLoop quits\n#\n#\tmainLoop is \"scheduled\" to run from GLib which is all set up in main()\n#\t\thowever, mainLoop is not a simple loop\n#\t\tit is called once per second by GLib then exits True\n#\t\tmainLoop exits by calling mainloop.quit() then returning False\n#\n#\tmain is tasks:\n#\t\tinstializing global variables that do not change over time\n#\t\tinstantiating objects\n#\t\tretriving nonvolatile information from dbus /Settings\n#\t\tstarting all threads\n#\t\tstart mainLoop\n#\n#\t\tthe code after mainloop.run() does not execute until mainLoop exits\n#\t\tat which time the activities necessarey for a clean exit are performed:\n#\n#\t\tan uninstall all function triggered by a flag on removable media\n#\t\t\tthis allows a blind uninstall of all packages (including SetupHelper)\n#\t\t\tin the event the GUI becomes unresponsive and the user has no access to the console\n#\t\tstopping all threads\n#\t\tPackageManager actual initialziation\n#\t\t\tis done by setting the package count in dbus /Settings to 0\n#\t\tremove dbus com.victronenergy.packageManager\n#\t\tuninstalling SetupHelper if requested above\n#\t\t\tdone by trigging a nohup backround task that sleeps for 30 seconds then installs the package\n#\t\treboot the system\n#\t\texit\n\n# persistent storage for mainLoop\npackageIndex = 0\nnoActionCount = 0\nlastDownloadMode = AUTO_DOWNLOADS_OFF\nbootInstall = False\nignoreBootInstall = False\nDeferredGuiEditAcknowledgement = None\nlastTimeSync = 0\n\nWaitForGitHubVersions = False\n\t\t\t\n# states for package.ActionNeeded\nREBOOT_NEEDED = 2\nGUI_RESTART_NEEDED = 1\nNONE = 0\n\ndef mainLoop ():\n\tglobal mainloop\n\tglobal PushAction\n\tglobal MediaScan\n\tglobal SystemReboot\t# initialized/used in main, set in mainloop, PushAction, InstallPackage\n\tglobal GuiRestart\t# initialized in main, set in PushAction, InstallPackage, used in mainloop\n\tglobal WaitForGitHubVersions  # initialized above, set in UpdateGitHubVersion used in mainLoop \n\tglobal InitializePackageManager # initialized/used in main, set in PushAction, MediaScan run, used in mainloop\n\tglobal RestartPackageManager # initialized/used in main, set in PushAction, MediaScan run, used in mainloop\n\tglobal DeferredGuiEditAcknowledgement # set in the handleGuiEditAction thread becasue the dbus paramter can't be set there\n\n\tglobal noActionCount\n\tglobal packageIndex\n\tglobal noActionCount\n\tglobal lastDownloadMode\n\tglobal bootInstall\n\tglobal ignoreBootInstall\n\tglobal lastTimeSync\n\tstartTime = time.time()\n\n\t# an unclean shutdown will not save the last known time of day\n\t#\twhich is used during the next boot until ntp can sync time\n\t#\tso do it here every 30 seconds\n\t# an old RTC\n\ttimeSyncCommand = '/etc/init.d/save-rtc.sh'\n\tif startTime > lastTimeSync + 30 and os.path.exists (timeSyncCommand):\n\t\ttry:\n\t\t\tsubprocess.Popen ( [ timeSyncCommand ],\n\t\t\t\t\tbufsize=-1, stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n\t\t\tproc.commiunicate ()\t# output ignored\n\t\texcept:\n\t\t\tpass\n\t\tlastTimeSync = startTime\n\n\tpackageName = \"none\"\n\n\tif DeferredGuiEditAcknowledgement != None:\n\t\tDbusIf.AcknowledgeGuiEditAction (DeferredGuiEditAcknowledgement)\n\t\tDeferredGuiEditAcknowledgement = None\n\n\n\t# auto uninstall triggered by AUTO_UNINSTALL_PACKAGES flag file on removable media\n\t#\tor SetupHelper uninstall was deferred\n\t# exit mainLoop and do uninstall in main, then reboot\n\t# skip all processing below !\n\tactionMessage = \"\"\n\tbootReinstallFile=\"/etc/venus/REINSTALL_PACKAGES\"\n\n\tcurrentDownloadMode = DbusIf.GetAutoDownloadMode ()\n\temptyPackageList = False\n\tcheckPackages = True\n\tautoInstall = False\n\tautoDownload = False\n\n\t# hold off all package processing if package list is empty\n\tif len (PackageClass.PackageList) == 0:\n\t\temptyPackageList = True\n\t\tcheckPackages = False\n\t\tpackageIndex = 0\n\n\t# if boot-time reinstall has been requiested by reinstallMods\n\t#\toverride modes and initiate auto install of all packages\n\t# ignore the boot reinstall flag if it's been done once and the flag removal failed\n\telif os.path.exists (bootReinstallFile) and not ignoreBootInstall:\n\t\t# beginning of boot install - reset package index to insure a complete scan\n\t\tif not bootInstall:\n\t\t\tbootInstall = True\n\t\t\tpackageIndex = 0\n\t\t\tlogging.info (\"starting boot-time reinstall\")\n\telif WaitForGitHubVersions:\n\t\tcheckPackages = False\n\n\t# don't look for new actions if uninstalling all packages or uninstalling SetupHelper\n\telif MediaScan.AutoUninstall or SetupHelperUninstall or RestartPackageManager:\n\t\tpass\n\t# not doing something special - use dbus values\n\telse:\n\t\tautoDownload = currentDownloadMode != AUTO_DOWNLOADS_OFF\n\t\tautoInstall = DbusIf.GetAutoInstall ()\n\n\t\t# download mode changed\n\t\t# restart at beginning of list and refresh all GitHub versions\n\t\tif currentDownloadMode != lastDownloadMode and currentDownloadMode != AUTO_DOWNLOADS_OFF:\n\t\t\tpackageIndex = 0\n\t\t\tcheckPackages = False\n\t\t\tUpdateGitHubVersion.SetPriorityGitHubVersion ('REFRESH')\n\t\t# save mode so changes can be detected on next pass\n\t\tlastDownloadMode = currentDownloadMode\n\n\t# make sure a new scan starts at beginning of list\n\tif not checkPackages:\n\t\tpackageIndex = 0\n\t# process one package per pass of mainloop\n\telse:\n\t\tDbusIf.LOCK (\"mainLoop 1\")\t\n\t\tpackageListLength = len (PackageClass.PackageList)\n\t\t# reached end of list - start over\n\t\tif packageIndex >= packageListLength:\n\t\t\tpackageIndex = 0\n\t\t\t# end of ONCE download - switch auto downloads off\n\t\t\tif currentDownloadMode == ONE_DOWNLOAD:\n\t\t\t\tDbusIf.SetAutoDownloadMode (AUTO_DOWNLOADS_OFF)\n\t\t\t\tcurrentDownloadMode = AUTO_DOWNLOADS_OFF\n\t\t\t# end of boot install\n\t\t\tif bootInstall:\n\t\t\t\tlogging.info (\"boot-time reinstall complete\")\n\t\t\t\tbootInstall = False\n\t\t\t\tif os.path.exists (bootReinstallFile):\n\t\t\t\t\ttry:\n\t\t\t\t\t\tos.remove (bootReinstallFile)\n\t\t\t\t\texcept FileNotFoundError:\n\t\t\t\t\t\tpass\n\t\t\t\t\texcept:\n\t\t\t\t\t\t# log the error and continue\n\t\t\t\t\t\t# set flag so we don't repeat the reinstall if the flag removal fails (until next boot)\n\t\t\t\t\t\tignoreBootInstall = True\n\t\t\t\t\t\tlogging.critical (\"could not remove the boot time reinstall flag: /etc/venus/REINSTALL_PACKAGES\")\n\n\t\tpackage = PackageClass.PackageList [packageIndex]\n\t\tpackageName = package.PackageName\n\t\tpackageIndex += 1\n\n\t\t# skip conflict checks if boot-time checks are bening made\n\t\tpackage.UpdateVersionsAndFlags (doConflictChecks = not bootInstall)\n\n\t\t# disallow operations on this package if anything is pending\n\t\tpackageOperationOk = not package.DownloadPending and not package.InstallPending\n\t\tif packageOperationOk and autoDownload and DownloadGitHub.DownloadVersionCheck (package):\n\t\t\t# don't allow install if download is needed - even if it has not started yet\n\t\t\tpackageOperationOk = False\n\t\t\tactionMessage = \"downloading \" + packageName + \" ...\"\n\t\t\tPushAction ( command='download' + ':' + packageName, source='AUTO' )\n\n\t\t# validate package for install\n\t\tif packageOperationOk and package.Incompatible == \"\" :\n\t\t\tinstallOk = False\n\t\t\t# one-time install flag file is set in package directory - install without further checks\n\t\t\toneTimeInstallFile = \"/data/\" + packageName + \"/ONE_TIME_INSTALL\"\n\t\t\tif os.path.exists (oneTimeInstallFile):\n\t\t\t\tos.remove (oneTimeInstallFile)\n\t\t\t\tinstallOk = True\n\t\t\t# auto install OK (not manually uninstalled) and versions are different\n\t\t\telif package.AutoInstallOk and package.PackageVersionNumber != package.InstalledVersionNumber:\n\t\t\t\tif autoInstall:\n\t\t\t\t\tinstallOk = True\n\t\t\t\t# do boot-time install only if the package is not installed\n\t\t\t\telif bootInstall and package.InstalledVersion == \"\":\n\t\t\t\t\tinstallOk = True\n\t\t\t\telif os.path.exists (\"/data/\" + packageName + \"/AUTO_INSTALL\"):\n\t\t\t\t\tinstallOk = True\n\n\t\t\tif installOk:\n\t\t\t\tpackageOperationOk = False\n\t\t\t\tactionMessage = \"installing \" + packageName + \" ...\"\n\t\t\t\tPushAction ( command='install' + ':' + packageName, source='AUTO' )\n\t\tDbusIf.UNLOCK (\"mainLoop 1\")\n\t# end if checkPackages\n\n\tDbusIf.LOCK (\"mainLoop 2\")\n\tactionsPending = False\n\tactionsNeeded = \"\"\n\tsystemAction = NONE\n\t# hold off reboot or GUI restart if any package has an action pending\n\t# collect actions needed to activage changes - only sent to GUI - no action taken\n\tfor package in PackageClass.PackageList:\n\t\tif package.DownloadPending or package.InstallPending:\n\t\t\tactionsPending = True\n\t\t# clear GitHub version if not refreshed in 10 minutes\n\t\telif package.GitHubVersion != \"\" and package.lastGitHubRefresh > 0 and time.time () > package.lastGitHubRefresh + NORMAL_GITHUB_REFRESH + 10:\n\t\t\tpackage.SetGitHubVersion (\"\")\n\n\t\tif package.ActionNeeded == REBOOT_NEEDED:\n\t\t\tactionsNeeded += (package.PackageName + \" requires REBOOT\\n\")\n\t\t\tsystemAction = REBOOT_NEEDED\n\t\telif package.ActionNeeded == GUI_RESTART_NEEDED:\n\t\t\tactionsNeeded += (package.PackageName + \" requires GUI restart\\n\")\n\t\t\tif systemAction != REBOOT_NEEDED:\n\t\t\t\tsystemAction = GUI_RESTART_NEEDED\n\n\tif systemAction == REBOOT_NEEDED:\n\t\tactionsNeeded += \"REBOOT system ?\"\n\telif systemAction == GUI_RESTART_NEEDED:\n\t\tactionsNeeded += \"restart GUI ?\"\n\n\t# don't show an action needed if reboot, etc is pending\n\tif SystemReboot or GuiRestart or RestartPackageManager or InitializePackageManager:\n\t\tDbusIf.DbusService['/ActionNeeded'] = \"\"\n\telse:\n\t\tDbusIf.DbusService['/ActionNeeded'] = actionsNeeded\n\n\tDbusIf.UNLOCK (\"mainLoop 2\")\n\n\tif actionsPending:\n\t\tnoActionCount = 0\n\telse:\n\t\tnoActionCount += 1\n\n\t# wait for two complete passes with nothing happening\n\t#\tbefore checking to see if mainLoop should quit\n\tif noActionCount >= 2:\n\t\tif SystemReboot or InitializePackageManager or GuiRestart\\\n\t\t\t\t or RestartPackageManager or MediaScan.AutoUninstall or SetupHelperUninstall:\n\t\t\t# already exiting - include pending operations\n\t\t\tif systemAction == REBOOT_NEEDED:\n\t\t\t\tSystemReboot = True\n\t\t\telif systemAction == GUI_RESTART_NEEDED:\n\t\t\t\tGuiRestart = True\n\t\t\tmainloop.quit()\n\t\t\treturn False\n\n\tif actionMessage != \"\":\n\t\tDbusIf.UpdateStatus ( actionMessage, where='PmStatus' )\n\telse:\n\t\tif emptyPackageList:\n\t\t\tidleMessage = \"no active packages\"\n\t\telif bootInstall:\n\t\t\tidleMessage = \"reinstalling packages after firmware update\"\n\t\telif WaitForGitHubVersions:\n\t\t\tidleMessage = \"refreshing GitHub version information\"\n\t\telif currentDownloadMode != AUTO_DOWNLOADS_OFF and autoInstall:\n\t\t\tidleMessage = \"checking for downloads and installs\"\n\t\telif currentDownloadMode == AUTO_DOWNLOADS_OFF and autoInstall:\n\t\t\tidleMessage = \"checking for installs\"\n\t\telif currentDownloadMode != AUTO_DOWNLOADS_OFF and not autoInstall:\n\t\t\tidleMessage = \"checking for downloads\"\n\t\telse:\n\t\t\tidleMessage = \"\"\n\t\tDbusIf.UpdateStatus ( idleMessage, where='PmStatus' )\n\n\t# enable the following lines to report execution time of main loop\n\t####endTime = time.time()\n\t####print (\"main loop time %3.1f mS\" % ( (endTime - startTime) * 1000 ), packageName)\n\n\t# to continue the main loop, must return True\n\treturn True\n# end mainLoop\n\n# uninstall a package with a direct call to it's setup script\n# used to bypass package list, and other processing\n#\n# do not use once package list has been set up\n\ndef\tdirectUninstall (packageName ):\n\tglobal SetupHelperUninstall\n\tglobal SystemReboot\n\tglobal GuiRestart\n\n\tif packageName == \"SetupHelper\":\n\t\tSetupHelperUninstall = True\n\n\tpackageDir = \"/data/\" + packageName\n\tsetupFile = packageDir + \"/setup\"\n\ttry:\n\t\tif os.path.isdir (packageDir) and os.path.isfile (setupFile) \\\n\t\t\t\tand os.access(setupFile, os.X_OK):\n\t\t\tproc = subprocess.Popen ( [ setupFile, 'uninstall', 'runFromPm' ],\n\t\t\t\t\t\tbufsize=100000, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True )\n\t\t\tproc.wait ()\n\t\t\t# forward stdout and stderr lines from setup script to console/log file\n\t\t\t#\tnote: all stdout lines will be output first, then all stderr lines\n\t\t\t#\t\tso won't be in cronological order !\n\t\t\tfor line in proc.stdout:\n\t\t\t\tlogging.info ( line.strip () )\n\t\t\tfor line in proc.stderr:\n\t\t\t\tlogging.error ( line.strip () )\n\t\t\treturnCode = proc.returncode\n\texcept:\n\t\tlogging.critical (\"could not uninstall \" + packageName)\n\telse:\n\t\tif returnCode == EXIT_REBOOT:\n\t\t\tSystemReboot = True\n\t\telif returnCode == EXIT_RESTART_GUI:\n\t\t\tGuiRestart = True\n\n\n# signal handler for TERM and CONT\n# this is needed to allow pending operations to finish before PackageManager exits\n# TERM sets RestartPackageManager which causes mainLoop to exit and therefore main to complete\n# TERM, then CONT is issued by supervise when shutting down the service\n# CONT handler differentiates a restart vs service down for logging purposes\n\ndef setPmRestart (signal, frame):\n\tglobal RestartPackageManager\n\tRestartPackageManager = True\n\ndef shutdownPmRestart (signal, frame):\n\tglobal RestartPackageManager\n\tglobal ShutdownPackageManager\n\tif RestartPackageManager:\n\t\tShutdownPackageManager = True\n\nsignal.signal (signal.SIGTERM, setPmRestart)\nsignal.signal (signal.SIGCONT, shutdownPmRestart)\n\n\n#\tmain\n#\n# ######## code begins here\n# responsible for initialization and starting main loop and threads\n# also deals with clean shutdown when main loop exits\n#\n\ndef main():\n\tglobal mainloop\n\tglobal SystemReboot\t# initialized/used in main, set in mainloop, PushAction, InstallPackage\n\tglobal GuiRestart\t# initialized in main, set in PushAction, InstallPackage, used in mainloop\n\tglobal InitializePackageManager # initialized in main, set in PushAction, used in mainloop\n\tglobal RestartPackageManager # initialized in main, set in PushAction, used in mainloop\n\tglobal ShutdownPackageManager\n\tglobal SetupHelperUninstall\n\tglobal WaitForGitHubVersions  # initialized in main, set in UpdateGitHubVersion used in mainLoop\n\tSystemReboot = False\n\tGuiRestart = False\n\tInitializePackageManager = False\n\tRestartPackageManager = False\n\tShutdownPackageManager = False\n\tSetupHelperUninstall = False\n\n\t# set logging level to include info level entries\n\tlogging.basicConfig( format='%(levelname)s:%(message)s', level=logging.INFO )\n\n\t# fetch installed version\n\tinstalledVersionFile = \"/etc/venus/installedVersion-SetupHelper\"\n\ttry:\n\t\tversionFile = open (installedVersionFile, 'r')\n\texcept:\n\t\tinstalledVersion = \"\"\n\telse:\n\t\tinstalledVersion = versionFile.readline().strip()\n\t\tversionFile.close()\n\t\t# if file is empty, an unknown version is installed\n\t\tif installedVersion ==  \"\":\n\t\t\tinstalledVersion = \"unknown\"\n\n\tlogging.info (\"PackageManager \" + installedVersion + \" starting\")\n\n\tfrom dbus.mainloop.glib import DBusGMainLoop\n\n\t# Have a mainloop, so we can send/receive asynchronous calls to and from dbus\n\tDBusGMainLoop(set_as_default=True)\n\n\t# get platform\n\tglobal Platform\n\tplatformFile = \"/etc/venus/machine\"\n\ttry:\n\t\tfile = open (platformFile, 'r')\n\texcept:\n\t\tPlatform = \"???\"\n\telse:\n\t\tmachine = file.readline().strip()\n\t\tif machine == \"einstein\":\n\t\t\tPlatform = \"Cerbo GX\"\n\t\tif machine == \"cerbosgx\":\n\t\t\tPlatform = \"Cerbo SGX\"\n\t\telif machine == \"bealglebone\":\n\t\t\tPlatform = \"Venus GX\"\n\t\telif machine == \"ccgx\":\n\t\t\tPlatform = \"CCGX\"\n\t\telif machine == \"canvu500\":\n\t\t\tPlatform = \"CanVu 500\"\n\t\telif machine == \"nanopi\":\n\t\t\tPlatform = \"Multi/Easy Solar GX\"\n\t\telif machine == \"raspberrypi2\":\n\t\t\tPlatform = \"Raspberry Pi 2/3\"\n\t\telif machine == \"raspberrypi4\":\n\t\t\tPlatform = \"Raspberry Pi 4\"\n\t\telif machine == \"raspberrypi5\":\n\t\t\tPlatform = \"Raspberry Pi 5\"\n\t\telif machine == \"ekrano\":\n\t\t\tPlatform = \"Ekrano GX\"\n\t\telse:\n\t\t\tPlatform = machine\n\t\tfile.close()\n\n\t# initialze dbus Settings and com.victronenergy.packageManager\n\tglobal DbusIf\n\tDbusIf = DbusIfClass ()\n\t\n\t# populate local package information from dbus settings\n\t#\tthis creates package class instances\n\tPackageClass.AddPackagesFromDbus ()\n\n\tglobal UpdateGitHubVersion\n\tUpdateGitHubVersion = UpdateGitHubVersionClass ()\n\n\tglobal DownloadGitHub\n\tDownloadGitHub = DownloadGitHubPackagesClass ()\n\t\n\tglobal InstallPackages\n\tInstallPackages = InstallPackagesClass ()\n\n\tglobal AddRemove\n\tAddRemove = AddRemoveClass ()\n\n\tglobal MediaScan\n\tMediaScan = MediaScanClass ()\n\n\t# initialze package list\n\t#\tand refresh versions before starting threads\n\t#\tand the background loop\n\n\tDbusIf.ReadDefaultPackagelist ()\n\n\tPackageClass.AddStoredPackages ()\n\t\n\t# make a pass through the package list to:\n\t#\tupdate local versions and flags\n\t#\t\t( this is needed anyway before mainLoop so do it here while we have the package )\n\t#\tremove any packages if its name is not valid\n\t#\t\t(invalid package names may be left over from a previous version)\n\t# \tremove any packages with their forced removal flag is set\n\t#\t\tpackage conflicts are sometimes resolved by uninstalling a package\n\t#\t\t\t(done in their setup script eg GuiMods force removes GeneratorConnector)\n\t#\tremove duplicate packages\n\t# could be time-consuming (uninstall, removal and checking all packages)\n\t# lock is really unecessary since threads aren't running yet\n\t#\n\t# if a package is removed, start at the beginning of the list again\n\n\twhile True:\n\t\tDbusIf.LOCK (\"main\")\n\t\trunAgain = False\n\t\texistingPackages = []\n\t\tfor (index, package) in enumerate (PackageClass.PackageList):\n\t\t\tpackageName = package.PackageName\n\t\t\t# valid package name\n\t\t\tif PackageClass.PackageNameValid (packageName):\n\n\t\t\t\tflagFile = \"/data/setupOptions/\" + packageName + \"/FORCE_REMOVE\" \n\t\t\t\t# forced removal flag\n\t\t\t\tif os.path.exists (flagFile):\n\t\t\t\t\tos.remove (flagFile)\n\t\t\t\t\tif os.path.exists (\"/etc/venus/installedVersion-\" + packageName):\n\t\t\t\t\t\tlogging.info ( \"uninstalling \" + packageName + \" prior to forced remove\" )\n\t\t\t\t\t\tdirectUninstall (packageName)\n\t\t\t\t\t# now remove the package from list\n\t\t\t\t\tlogging.warning ( \"forced remove of \" + packageName )\n\t\t\t\t\tPackageClass.RemovePackage (packageIndex=index)\n\t\t\t\t\trunAgain = True\n\t\t\t\t\tbreak\n\t\t\t\telif packageName in existingPackages:\n\t\t\t\t\tlogging.warning ( \"removing duplicate \" + packageName )\n\t\t\t\t\tPackageClass.RemovePackage (packageIndex=index, isDuplicate=True)\n\t\t\t\t\trunAgain = True\n\t\t\t\t\tbreak\n\n\t\t\t# invalid package name (including a null string) so remove the package from the list\n\t\t\telse:\n\t\t\t\tlogging.warning ( \"removing package with invalid name \" + packageName )\n\t\t\t\tPackageClass.RemovePackage (packageIndex=index)\n\t\t\t\trunAgain = True\n\t\t\t\tbreak\n\n\t\t\t# package not removed above - add its name to list that will be checked for duplicates\n\t\t\texistingPackages.append (packageName)\n\n\t\tDbusIf.UNLOCK (\"main\")\n\t\tif not runAgain:\n\t\t\tbreak\n\tdel existingPackages\n\n\tDbusIf.UpdateDefaultPackages ()\n\n\t#### start threads - must use LOCK / UNLOCK to protect access to DbusIf from here on\n\tUpdateGitHubVersion.start()\n\tDownloadGitHub.start()\n\tInstallPackages.start()\n\tAddRemove.start()\n\tMediaScan.start ()\n\n\t# call the main loop - every 1 second\n\t# this section of code loops until mainloop quits\n\tGLib.timeout_add(1000, mainLoop)\n\tmainloop = GLib.MainLoop()\n\tmainloop.run()\n\n\n\t#### this section of code runs only after the mainloop quits (LOCK / UNLOCK no longer necessary)\n\n\t# output final prompts to GUI and log\n\tDbusIf.DbusService['/ActionNeeded'] = \"\"\n\tDbusIf.SetEditStatus (\"\")\n\tDbusIf.AcknowledgeGuiEditAction ('')\n\tmessage = \"\"\n\tif MediaScan.AutoUninstall:\n\t\tmessage = \"UNINSTALLING ALL PACKAGES & REBOOTING ...\"\n\t\tlogging.warning (\">>>> UNINSTALLING ALL PACKAGES & REBOOTING...\")\n\telif SetupHelperUninstall:\n\t\tmessage = \"SetupHelper UNINSTALLED\"\n\t\tlogging.critical (\">>>> SetupHelper UNINSTALLED\")\n\telif InitializePackageManager:\n\t\tif SystemReboot:\n\t\t\tmessage = \"initializing and REBOOTING ...\"\n\t\t\tlogging.info (\">>>> initializing PackageManager and REBOOTING SYSTEM\")\n\t\telse:\n\t\t\tlogging.info (\">>>> initializing PackageManager ...\")\n\t\t\tmessage = \"initializing and restarting PackageManager ...\"\n\telif SystemReboot:\n\t\tmessage = \"REBOOTING SYSTEM ...\"\n\t\tlogging.info (\">>>> REBOOTING SYSTEM\")\n\telif GuiRestart:\n\t\tmessage = \"restarting GUI and Package Manager...\"\n\telif ShutdownPackageManager:\n\t\tmessage = \"shutting down PackageManager ...\"\n\telif RestartPackageManager:\n\t\tmessage = \"restarting PackageManager ...\"\n\tDbusIf.UpdateStatus ( message=message, where='PmStatus' )\n\tDbusIf.UpdateStatus ( message=message, where='Editor' )\n\n\t# stop threads, remove service from dbus\n\tlogging.info (\"stopping threads\")\n\tUpdateGitHubVersion.StopThread ()\n\tDownloadGitHub.StopThread ()\n\tInstallPackages.StopThread ()\n\tAddRemove.StopThread ()\n\tMediaScan.StopThread ()\n\n\ttry:\n\t\tUpdateGitHubVersion.join (timeout=1.0)\n\t\tDownloadGitHub.join (timeout=1.0)\n\t\tInstallPackages.join (timeout=1.0)\n\t\tAddRemove.join (timeout=1.0)\n\t\tMediaScan.join (timeout=1.0)\n\texcept:\n\t\tlogging.critical (\"one or more threads failed to exit\")\n\t\tpass\n\n\t# if initializing PackageManager persistent storage, set PackageCount to 0\n\t#\twhich will cause the package list to be rebuilt from packages found in /data\n\t#\tuser-specified Git Hub user and branch are lost\n\tif InitializePackageManager:\n\t\tDbusIf.DbusSettings['packageCount'] = 0\n\n\t# com.victronenergy.packageManager no longer available after this call\n\tDbusIf.RemoveDbusService ()\n\n\t# auto uninstall triggered by AUTO_UNINSTALL_PACKAGES flag file on removable media\n\tif MediaScan.AutoUninstall:\n\t\t# uninstall all packages EXCEPT SetupHelper which is done later\n\t\tfor path in os.listdir (\"/data\"):\n\t\t\tdirectUninstall (path)\n\t\tSystemReboot = True\n\n\tif SystemReboot:\n\t\ttry:\n\t\t\t# insure PackageManager service does not restart when reboot is needed\n\t\t\tproc = subprocess.Popen ( [ 'svc', '-o', '/service/PackageManager' ] )\n\t\t\t# TODO: add -k for debugging - outputs message but doesn't reboot\n\t\t\tproc = subprocess.Popen ( \"nohup sleep 5; shutdown -r now PackageManager is REBOOTING SYSTEM ... &\", shell=True )\n\t\texcept:\n\t\t\tlogging.critical (\"system reboot command failed\")\n\telif GuiRestart:\n\t\tif os.path.exists (\"/service/start-gui\" ):\n\t\t\tcommand = [ 'svc', '-t', '/service/start-gui' ]\n\t\telse:\n\t\t\tcommand = [ 'svc', '-t', '/service/gui' ]\n\t\ttry:\n\t\t\tproc = subprocess.Popen ( command )\n\t\texcept:\n\t\t\tlogging.critical (\"GUI restart failed\")\n\n\n\tlogging.info (\">>>> PackageManager exiting\")\n\n#### Initial entry point for program\nmain()\n\n\n\n\n\n"
  },
  {
    "path": "ReadMe.md",
    "content": "# Overview\n\nThe SetupHelper package provides:\n  - a mechanism to automatically reinstall packages following a Venus OS update\n  - an automatic update mechanism to keep packages up to date from GitHub archives or USB stick\n\t- control of the automatic download and install from the GUI\n\t- add and remove packages from the system\n\t- manual download, install, uninstall from the GUI\n\t- checks for package conflicts and prevents one package installing over another when the same files are modified\n\t- provides a \"conflict resolution\" option when such conflicts exist\n\n  - a \"blind\" install of SetupHelper from SD/USB media\n\n  - a blind uninstall mechanism which optionally includes reinstalling Venus OS\n\n  - backup and restore SOME settings from `com.victronenergy.settings`\n\t\n\tThis includes custom logos and copying logs to removable media\n\t- SetupHelper\n\t- PackageManager\n\t- gui\n\n  - Restart or initialize PackageManager\n  - Restart the GUI\n\n> [!NOTE]\n> Support for firmware prior to v3.10 has been dropped starting with SetupHelper v8.10\n> if you are running older versions, change the branch/tag to preV3.10support\n> for any packages you wish to run on that firmware\n>\n> While this branch will remain active, there will be no features added to it\n> and only serious bug fixes will be applied.\n\n# Changes\n\n**SetupHelper v9**\n\t- changes to logging to comform to Victron rules (see below)\n\n**SetupHelper v8**\n  - adds the ability for multiple packages to modify the same file\n  \n\t  - Packages must be written to \"patch\" a file rather than \"replace\" it\n\n**SetupHelper v7.0**\n- adds a conflict resolution mechanism.\n  \n  - Packages can identify known conflicts with other packages with a \"packageDependencies\" list\n  - One package can specify that other packages must be uninstalled or installed\n\tbefore allowing the package to be installed\n  - PackageManager also checks all files that will be modified to see if another package has already modified the same file.\n    \n    > [!NOTE] \n    > All packages should be uninstalled, then reinstalled to create the necessary\n    > information for these file-based conflicts to be identified.\n    \n    If a conflict exists it is reported on in the PackageManager menus and install is blocked. These conflicts can be resolved from within the Package editor menu.\n\n**SetupHelper v6.0**\n\n> [!NOTE]\n> SetupHelper v6.0 changes significantly from prevous versions\n\n- providing more automatic installation and installation of files, services and dBus Settings\n\t- v6.0 will install older packages but package setup scripts that utilize the new\n\tautomated install and uninstall functions **will not work with SetupHelper v5.x**\n\t\n\t  For this reason, packages that rely on the v6.0 setup helper functionality\n\tshould also include a copy of the **HelperResources** found in SetupHelper v6.0 and newer\n\t\n\t  Sourcing these helpers has also changed in v6.0. But there is also a backward\n\tcompatible hook for older packages.\n\t\n\t  The new sourcing mechanism can be found in the file `SetupHelper/HelperResources/forSetupScript`.\n\n# Logging\n\nVictron Energy has requested all packages refrain from writing to a log file in order to prevent\nrunaway processes (e.g., services that crash and restart constantly) filling available space on /data.\n\nTherefore, when a setup script is run from the console, messages appear on the console\nbut **are not logged**\n\nWhen the script is run from PackageManager, PackageManager forwards the script's messages to its log file.\n\nThere are a few situations where a script will write directly to the PackageManager log file:\n\n- **reinstallMods** is called from rcS.local to reinstall PackageManager after a firmware update\n\n- **blind install**  and **blind uninstall**\n\n\n# Helper resources\n\nOther packages use \"helper resources\" provided by SetupHelper\n\nHelper Resources simplify the package's setup script and include hooks that PackageManager uses to control installs and uninstalls.\n\nMore information about Setup Helper and how to create packages that use it can be found in the file PackageDevelopmentGuidelines.md in the package directory.\n\n# Blind Install:\n\nBy far, the easiest way to install SetupHelper is the \"blind install\" which requires no command-line interaction.\n\n1. Download `venus-data-SetupHelperInstall.tgz` from the SetupHelper GitHub [repo](https://github.com/kwindrem/SetupHelper/raw/main/venus-data-SetupHelperInstall.tgz).\n> [!NOTE]\n> Mac OS and Safari are set by default to unzip packages.\n> The Open \"safe\" files after downloading (bottom of Safari Preferences General)\n> must be disabled in order to retain the zip file. \n\n2. copy it to the root of a freshly formatted SD card or USB memory stick\n3. place the media in the GX device (Cerbo, CCGX, etc)\n4. reboot the GX device and allow the system to display the GUI\n\n        - you should find the Package Manager menu at the bottom of the Settings menu\n\n5. **REMOVE THE MEDIA** from the GX device after you see the GUI displayed\n\n          Mechanisms are in place to prevent reinstallation, but removal is still a good idea!\n\nYou should find the Package Manager menu at the bottom of the Settings menu\n\n        \n\nAnother way to install SetupHelper is to use the following from the command line of the GX device:\n\n```bash\nwget -qO - https://github.com/kwindrem/SetupHelper/archive/latest.tar.gz | tar -xzf - -C /data\nrm -rf /data/SetupHelper\nmv /data/SetupHelper-latest /data/SetupHelper\n/data/SetupHelper/setup\n```\n\nYou can also use the above procedure to install other packages. \nSimply substiture SetupHelper with the package name (e.g., GuiMods) in the above commands.\nHowever, using the PackageManager menus is by far easier.\n\nOnce SetupHelper is installed, updates to it and other packages can be performed through the Classic UI using the PackageManager menus.\n\n> [!CAUTION]\n>  Package Manager allows uninstalling SetupHelper.\n>\n>  This can not be undone since the menus to control Package Manager will go away.\n\tYou would need to use the Blind Install or run /data/SetupHelper/setup again to reinstall SetupHelper\n>\t\n> Note that removal does not actually remove the package so other setup scripts\n> will continue to function.\n\n> [!NOTE]\n> You can install other packages using wget as described above.\n> Or you can download the .tgz file and put that on a USB stick and plug that into the GX device.\n> \n> PackageManager will detect the file and install the package.\n\t\t\n\n# ssh access:\n\nSetting up ssh access with ssh keys is highly recommended for any system,\nbut especially when installing third party extensions to Venus OS.\nAttaching a serial terminal for direct console access is another option,\nespecially if you don't have a network setup.\n\n[This document](https://www.victronenergy.com/live/ccgx:root_access) describes ssh access and also serial terminal connections on Cerbo GX. \n\nRemote ssh access is now available via tailscale using the **TailscaleGX** package\n\n# System Recovery:\n\nIt is unlikely, but some users have reported a package install leaving their system unresponsive or with a nonfunctional GUI (white screen). In this case, your options depend on the current state of the system.\n\n1. (as always) reboot. This may clear the problem.\n\n2. if you have a functioning GUI (either locally or via remote console, see if you can access the PackageManager menu.\n    - If so, you can remove pacakges one at a time from there.\n    - If you find an offeding package, post an issue to the GitHub repo for that package and include:\n\t    - Platform (Cerbo, CCGX, Raspberry PI, etc)\n\t    - Venus OS firmware version\n\t    - Run a Settings backup and post the logs.zip file on the removable media.\n    - Remove SetupHelper last since once you do, you loose the PackageManager menus!\n\n3. if you have terminal or ssh access, try running the package setup scripts to uninstall packages one at a time.\n\n4. try booting to the previous Venus OS version (in Stored backup firmware)\n\tThen perform a fresh Online firmware update to the latest version or use the .swu update via removable media.\n\n\tUse the Settings / Firmware / Stored backup firmware menu if you have GUI access.\n\n\tIf you don't have GUI access, you can also switch to the backup version from the command line:\n\t\n\t```bash\n\t/opt/victronenergy/swupdate-scripts/set-version.sh 2\n    ```\n    \n\tYou can also force a firmware update from the command line if you have ssh or terminal access:\n\t- For on-line updates:\n\t  ```bash\n\t  /opt/victronenergy/swupdate-scripts/check-swupdate.sh -force -update\n\t  ```\n\t- For updates from removable media:\n\t  ```bash\n\t  /opt/victronenergy/swupdate-scripts/check-swupdate.sh -force -update -offline\n\t  ```\n\n5. If PackageManager is still running, it will detect a file named AUTO_UNINSTALL_PACKAGES on removable media.\n   - Create a file of that name (no extension, content unimportant) on a USB memory stick or SD card and insert this into the GX device.\n\n   - The system should eventually reboot. In most cases, this should occur within 1-2 minutes.\n   - After reboot, the system should come up in the stock configuration with no packages installed.\n\n   - If the system does not reboot, it is likely PackageManager is no longer running, so try other options.\n\n   - Remember to remove the media containing the `AUTO_UNINSTALL_PACKAGES` file to this will be repeated the next time PackageManager runs.\n\n6. perform the Blind uninstall procedure below.\n\n**Finally:**\n- If you are running on a Raspberry PI, you can reimage the system SD card.\n\t\n- If you have a Cerbo, you can reimage it using this procedure:\n\t\thttps://community.victronenergy.com/questions/204255/cerbo-gx-bricked-how-to-recover.html\n\n> [!NOTE]\n> This will wipe out all settings and you'll need to reconfigure the GX device from scratch.\n\n- The Victron \"restore factory default\" procedure can be used to will wipe out all settings.\n  - You'll need to reconfigure the GX device from scratch.\n  - However, it will NOT replace the operating system and Victron application, nor will it uninstall any packages.\n  - You will most likely be locked out of ssh access since log-in information and ssh keys\n\tare stored in the /data partition which is completey erased by this procedure.\n  - For this reason, I do not recommend using this as part of your attempt to recover a system with no GUI.\n\n\n# Blind UNINSTALL:\n\nA blind uninstall mechanism is provided to recover a system with an unresponsive GUI (white screen) or no ssh/terminal access.\nThis will run all package setup scripts to uninstall that package from system files.\n\nIn addition to uninstalling all packages, the blind uninstall can optionally reinstall VenusOS.\nTo do so, include a `.swu` file for the platform and desired firmware version on the SAME removable media\n\nThe archive for this is named `venus-data.UninstallAllPackages.tar.gz`.\n\n  1. Copy `venus-data.UninstallAllPackages.tar.gz` to a USB memory stick or SD card\n  2. Plug the removable media into the GX device\n  3. Reboot, wait 2 minutes and reboot a second time\n  4. When the system automatically reboots after the second manual one, remove the media.\n     You should eventually see the GUI on the local display if there is one\n     or be able to connect via remote console.\n\n> [!CAUTION]\n> Removing media or power cycling the GX device during the uninstall,\n> especially if reinstalling firmware could render the system unresponsive!\n> Wait to see the GUI before removing media or power cycling.\n\nNote that a firmware update can take several minutes to complete but will eventually reboot.\n\nWhen the blind uninstall finishes, `venus-data.UninstallAllPackages.tar.gz` file on the removable media\nis renamed (adding .XXX) so that the blind install will run only once.\nThis renaming is necessary to prevent a loop where the system uninstalls and reboots.\n\n# System automatic configuration and package installation:\n\nIt is possible to use SetupHelper to set up a new system based on a template saved from a working system.\n  - Setup the working system the way you want the new system to behave including custom icons,\n  - then perform a Settings backup.\n  - Remove the flash drive from the GX device and plug into a computer that has internet access.\n  - Copy `venus-data.tgz` from the SetupHelper GitHub repo to the same flash drive.\n  - If you wish packages to also be installed, copy the package -latest.tgz file from those repos as well.\n  - Create `SETTINGS_AUTO_RESTORE` on the flash drive (contents don't matter - file may be empty).\n  - Create `AUTO_INSTALL_PACKAGES` on the flash drive as well.\n  - Place the flash drive into the GX device to be configured and reboot (once for v2.90 or twice for prior versions).\n  - **REMOVE THE FLASH DRIVE** after you have verified that all packages have been installed (check Active packages in PackageManager).\n"
  },
  {
    "path": "blindInstall/SetupHelperVersion",
    "content": "v9.4\n"
  },
  {
    "path": "blindInstall/blindInstall.sh",
    "content": "#!/bin/bash\n\n# this script is part of a \"blind install\" archive which installs SetupHelper\n#\n# Simply inserting media into the GX device and rebooting once will install SetupHelper\n#\n# the process makes use of the Venus OS update-data.sh script run during system boot\n# archives named \"venus-data.tgz\" are unpacked during boot\n# overriting matching content in /data\n#\n# this archive unpacks to:\n#\t/data/SetupHelper-blind to avoid overwriting an existing copy of SetupHelper\n#\t/data/rc for the pre/post scripts\n# if versions of /data/SetupHelper-blind and the installed version of SetupHelper\n#\tDIFFER, OR if SetupHelper is NOT INSTALLED,\n#\tSetupHelper-blind replaces SetupHelper and the setup script is run\n#\n#\tpre-hook.sh and post-hook.sh scripts are run before and after the archive is unpacked\n# \t/data/rcS.local is saved in pre-hook.sh and restored in post-hook.sh.\n# \tThe /data/rcS.local file included in the archive is never executed\n# \tIn stead, post-hook.sh performs the version checks and calls blindInstall.sh\n#\t\tif appropriate. This eliminates the second reboot !\n#\tIn order to check versions prior to unpacking the archive,\n#\t\tthe SetupHelper version is duplicated in the rc folder which unpacks to /data\n#\t\tBEFORE the SetupHelper-blind is unpacked.\n#\n# blindInstall.sh is run in the background so it can wait for dbus Settings resources\n# to become available before running the package install script.\n#\n\nsource \"/data/SetupHelper-blind/HelperResources/EssentialResources\"\nlogToConsole=false\n\nlogMessage \"starting\"\n\n# wait until dbus settings are active\nwhile [ $(dbus -y | grep -c \"com.victronenergy.settings\") == 0 ]; do\n    logMessage \"waiting for dBus settings\"\n    sleep 1\ndone\n\nsleep 2\n\nsetupHelperBlind='/data/SetupHelper-blind'\nsetupHelperStored='/data/SetupHelper'\n\n# move the extracted archive into position and run the setup script\nif [ -e \"$setupHelperBlind\" ]; then\n\tif [ -e \"$setupHelperStored\" ]; then\n\t\tlogMessage \"removing previous SetupHelper\"\n\t\trm -rf \"$setupHelperStored\"\n\tfi\n\tlogMessage \"moving SetupHelper (from blind archive) into position\"\n\tmv \"$setupHelperBlind\" \"$setupHelperStored\"\nelse\n\tlogMessage \"SetupHelper archive not found - no changes to package\"\nfi\n\n# run the setup script\nif [ -f \"$setupHelperStored/setup\" ]; then\n\tlogMessage \"installing SetupHelper\"\n\t\"$setupHelperStored/setup\" install auto\nelse\n\tlogMessage \"error - can't install SetupHelper\"\nfi\n\n# remove the blind install SetupHelper from the archive if still present\nrm -rf \"$setupHelperBlind\"\n\nlogMessage \"completed\"\n\n"
  },
  {
    "path": "blindInstall/post-hook.sh",
    "content": "#!/bin/bash\n\n# this script is part of a \"blind install\" archive which installs SetupHelper\n#\n# refer to blindInstall.sh for more an explaination\n#\n\nlogDir=\"/var/log/PackageManager\"\nlogFile=\"$logDir/current\"\nif ! [ \"$logDir\" ]; then\n\tmkdir -P \"$logDir\"\nfi\nlogMessage ()\n{\n\techo \"$*\"\n\techo \"blind install post-hook.sh: $*\" | tai64n >> \"$logFile\"\n}\n\n\nlogMessage \"starting\"\n\n# run the blind install script from the SetupHelper-blind\nscript=\"/data/SetupHelper-blind/blindInstall/blindInstall.sh\"\nif [ -f \"$script\" ]; then\n\tlogMessage \"running blindInstall.sh as background process\"\n    nohup \"$script\" > /dev/null &\nfi\n\nlogMessage \"completed\"\n"
  },
  {
    "path": "blindInstall/pre-hook.sh",
    "content": "#!/bin/bash\n\n# this script is part of a \"blind install\" archive which installs SetupHelper\n# refer to blindInstall.sh for more an explaination\n#\n\nlogDir=\"/var/log/PackageManager\"\nlogFile=\"$logDir/current\"\nif ! [ \"$logDir\" ]; then\n\tmkdir -P \"$logDir\"\nfi\nlogMessage ()\n{\n\techo \"$*\"\n\techo \"blind install pre-hook.sh: $*\" | tai64n >> \"$logFile\"\n}\n\n\nlogMessage \"starting\"\n\nscriptDir=\"$( cd \"$(dirname $0)\" >/dev/null 2>&1 ; /bin/pwd -P )\"\nblindVersionFile=\"$scriptDir/SetupHelperVersion\"\ninstalledVersionFile='/etc/venus/installedVersion-SetupHelper'\nsetupHelperStored='/data/SetupHelper'\n\n# remove GitHub project data just in case it ends up on the target\n#\t(it's large (about 20 MB) and could get in the way of package replacement\nrm -rf $setupHelperStored/.git\n\ndoInstall=false\n# SetupHelper is currently stored in /data\n# check to see if it needs to be updated\nif [ -d \"$setupHelperStored\" ]; then\n\tif [ -f \"$blindVersionFile\" ]; then\n\t\tblindVersion=$(cat \"$blindVersionFile\")\n\telse\n\t\tlogMessage \"ERROR: no blind version\"\n\t\tblindVersion=\"\"\n\tfi\n\tif [ -f \"$installedVersionFile\" ]; then\n\t\tinstalledVersion=$(cat \"$installedVersionFile\")\n\telse\n\t\tinstalledVersion=\"\"\n\tfi\n\n\tif [ \"$installedVersion\" != \"$blindVersion\" ]; then\n\t\tdoInstall=true\n\tfi\n# no SetupHelper found, skip version checks and install\nelse\n\tdoInstall=true\nfi\n# returning with 0 will trigger unpacking and run post-hook.sh\nif $doInstall ; then\n\tlogMessage \"completed - will do install\"\n\texit 0\n# returning non-zero will prevent unpacking\n# there won't be an archive to unpack andpost-hook.sh will NOT run \nelse\n\tlogMessage \"completed - unstall not needed - skipping unpack and install\"\n\texit -1\nfi\n"
  },
  {
    "path": "blindInstall/rcS.localForUninstall",
    "content": "#!/bin/bash\n\n# this script is part of a \"blind UNINSTALL\" archive which UNINSTALLS all packages\n# Packages are not removed but marked so PackageManager does not auto intall later\n#\n# Venus OS will also be reinstalled if a suitable .swu file is found on removable media\n\n# log activity\nlogDir=\"/var/log/PackageManager\"\nlogFile=\"$logDir/current\"\nif ! [ \"$logDir\" ]; then\n\tmkdir -P \"$logDir\"\nfi\nlogMessage ()\n{\n\techo \"blindUninstall: $*\"\n\techo \"blindUninstall: $*\" | tai64n >> \"$logFile\"\n}\n\nlogMessage \"--- starting blind uninstall\"\n\n# check to see if Venus OS will be reinstalled - actuall reinstall will be done later\nswCheckOutput=$(/opt/victronenergy/swupdate-scripts/check-updates.sh -offline -force -check)\nif (( $? == 0 )); then\n\treinstallVenusOs=true\n\tswUpdateVersion=$(echo $swCheckOutput | awk '{print $NF}')\nelse\n\treinstallVenusOs=false\n\tswUpdateVersion=\"none\"\nfi\n\npackages=$(ls -d /data/*)\n# run the script to anything that looks like a package (version and setup files)\nfor package in $packages; do\n\tif [ -f \"$package/version\" ] && [ -f \"$package/setup\" ]; then\n\t\tpackageName=$(basename $package)\n\t\tlogMessage \"uninstalling $packageName\"\n\t\t\"$package/setup\" uninstall deferReboot deferGuiRestart auto\n\tfi\ndone\n\n# remove all installed info in case some were missed in loop above\nrm -f /etc/venus/installedVersion*\nrm -rf \"/etc/venus/installedModifications\"\n\n# rename archive on removable media to prevent blindInstall from running again\ndrives=$(ls /run/media/)\nfor drive in $drives ; do\n\tarchive=\"/run/media/$drive/venus-data-UninstallAllPackages.tgz\"\n\tif [ -f \"$archive\" ]; then\n\t\tlogMessage \"renaming venus-data-UninstallAllPackages.tgz so blindUninstall won't run again\"\n\t\tmv \"$archive\" \"$archive.XXX\"\n\tfi\ndone\n\n# reinstall Venus OS - done in background so this script can clean up and exit without disrupting the software update\nif $reinstallVenusOs ; then\n\tlogMessage \"reinstalling Venus OS $swUpdateVersion\"\n\tnohup sh -c 'sleep 1; /opt/victronenergy/swupdate-scripts/check-updates.sh -offline -force -update' > /dev/null &\n# reboot if not reinstalling Venus OS\nelse\n\tlogMessage \"rebooting ...\"\n\tnohup sh -c 'sleep 1; reboot' > /dev/null &\nfi\n\n# don't run this script again !\nrm -f /data/rcS.local\n\nlogMessage \"--- ending blind uninstall\"\n"
  },
  {
    "path": "changes",
    "content": "v9.4:\n\tadded support for Raspberry PI 5 platform\n\nv9.3:\n\tfixed: patch error on RPI 5\n\nv9.2:\n\tfixed: install failure during manual install does not force an uninstall\n\tadd support for v3.70~71\n\nv9.1:\n\tfixed: can't access package editor menu on firmware prior to v3.6 (regression in v9.0)\n\nv9.0:\n\trewrite to insure logging is all done via multilog\n\t\ttherefore, NO LOGGING when setup scripts are run from command line !!!!!!!\n\tupdated blind install/uninstall\n\tremoved support for firmware prior to v3.00 in various places\n\nv8.34\n\tfixed: PackageManager Edit menu: buttons on bottom row sometimes missing\n\tfixed: syntax warning nv firmware 3.60 - warning only, everything working\n\nv8.33:\n\tfixed: boot-time reinstall flag removal failure caused PackageManager to crash\n\t\tif the flag could not be removed\n\t\tthis would only happen if the file or partition did not have write permission\n\nv8.32:\n\tfixed: packages reinstalled at boot time with Auto Install off\n\t\tshould only install if not currently installed and not manually uninstalled\n\nv8.31:\n\tfixed: installedFileList not being created which may leave modified files after uninstall\n\tfixed: .NO_ORIG files not removed when uninstalling\n\nv8.30:\n\tfixed: generator service not restarted\n\t\t(moved from dbus-generator-starter to dbus-generator)\n\nv8.29:\n\tfixed: active file not always updated by a patched file - SILENTLY !\n\nv8.28:\n\tremove venus-os_ngrok from default package list\n\tfirst compatible version is now v3.10\n\nv8.27:\n\tsupport the change to QtQuick 2 first used in v3.60~18\n\nv8.26:\n\tadded link to IncludeHelpers so that old packages can find correct file to source\n\nv8.25:\n\tchanged ReadMe to a markup document\n\tincluded PackageDevelopmentGuidelines.md in the package (previously on DropBox)\n\nv8.24:\n\t8.23 did not have the blind install files\n\nv8.23:\n\tadd code to updatePackage and CommonResources to handle directory renames\n\t\t(e.g., dbus-generator-starter to dbus-generator)\n\tfixed: PackageManager crash if version string contains invalid characters\n\tupdatePackage: fixed: USE_ORIGINAL flag files not created in some cases\n\nv8.22:\n\tfixed: missing log file and directories are not created\n\t\tuntil the PackageManager service starts\n\t\tso messages logged before that are not present in the log file\n\nv8.21:\n\tfixed: patch error for some files if package is reinstalled\n\nv8.20:\n\tadd support for v3.50~22 (HTML style sheet in different location)\n\tadd dbus-pi package to defaults list\n\nv8.19:\n\tfixed: system reboots after user chooses to reboot later\n\nv8.18:\n\tfixed: errors when installing RemoteGPIO\n\nv8.17:\n\tfixed: adding dbus setting didn't accommodate values starting in --\n\tfixed: generator service was restarting unnecessarily when installing packages\n\nv8.16:\n\tfixed: no previous patch file error created in v8.15\n\nv8.15:\n\tfixed: GUI restart doesn't always occur after install\n\tfixed: previouis patches file creation fails on first install\n\t\tbecause root fs was stil read only\n\nv8.14:\n\tfixed: crash when installing packages (introduced in v8.11)\n\nv8.12:\n\treadded blind install files\n\nv8.11:\n\timproved install/uninstall error handling in endScript ()\n\tfixed: GUI and other service restarts not always happening\n\nv8.10:\n\tmoved velib_python in SetupHelper to a single version\n\tdropping support for firmware earlier than v3.10\n\nv8.9:\n\tfixed: further changes for the remote GUI issue\n\nv8.8:\n\tfixed: GuiMods web GUI (v1) broken (patched file permissions incorrect)\n\nv8.7:\n\tupdatePackage: always rebuild patch files\n\tprovide version-dependent velib_phthon for this and other packages\n\nv8.6:\n\tfixed: persistent download pending message after a download fails\n\nv8.5:\n\tfixed typo in version string (v8.4 was \"v8.3=4\")\n\nv8.4:\n\tFixed: package install fails if it has older setup script\n\t\t(for a while, packages could have their own helper files)\n\nv8.3:\n\tadded GitHub check frequency: 10 minutes, hourly, daily\n\t\treduces network bandwidth\n\nv8.2:\n\tfixed: auto downloads can happen even when off (introduced in v8.1)\n\nv8.1:\n\tfixed: GitHub version refresses occuring too fast (~10 seconds vs 10 minutes)\n\nv8.0:\n\tallow multiple packages to modify the same file\n\tallow multiple patch files for each active file\n\tfixed: PackageManager hangs if there is no setup script in package directory\n\tfixed: \"Once\" download scan doesn't check all packages\n\tfixed: PackageManager hangs on Python 2 (Venus OS prior to v2.80)\n\tuse HelperResources only from SetupHelper (not from package directory)\n\t\tthis was necessary because /data/SetupHelper/patch\n\t\tis now used in place of the stock patch executable\n\tmoved SetupHelper logging to /var/log/PackageManager/current\n\t\tfrom /var/log/SetupHelper\n\tadded Recheck button for errors discovered in setup script prechecks\n\tPackageManager now completes pending operations before exiting\n\tadd TailscaleGX to default package list\n\tupdatePackage: added patch options including MANUAL to prevent automatic patch updates\n\tupdatePackage: rewrite update file sets loop for speed improvement\n\nv7.18:\n\tfixed: only first service is uninstalled\n\nv7.17:\n\tservices not always uninstalled\n\nv7.16:\n\tfixed: PackageManager hangs with package add\n\nv7.15:\n\tfixed: GitHub version not refreshed when user/branch change\n\tfixed: old blind install\n\nv7.14:\n\tfixed: incompatible message not cleared when package no longer incompatible\n\nv7.13:\n\tfixed: PackageManager doesn't install packages after firmware update\n\nv7.12:\n\tfixed: PackageManager hangs if there is no setup script in package directory\n\nv7.11:\n\tfixed: conflicts not cleared when they have been resolved\n\nv7.10:\n\tfixed: services with dash in the name do not install/uninstall\n\nv7.9:\n\tadded blind install .tgz files\n\nv7.8:\n\tfixed: packages getting downloaded when not needed + PackageManger crash\n\t\tresults in corrupted pacakges\n\nv7.7:\n\tfixed: remove duplicates a package in Active packages\n\nv7.6:\n\tfixed: Package editor menus shows Now / Later in stead of Proceed / Cancel\n\t\tfor Show Details\n\nv7.5:\n\tfixed: PackageManager restarts when removing package\n\nv7.4:\n\tversion bump -- HelperResources version was not updated in v7.3\n\nv7.3:\n\tfixed: GitHub versions are not refreshed when reentering the Active Packages menu\n\tfixed: unrecognized command showDetails\n\tfixed: can't remove packages from Package editor menu\n\timprove reporting of restart/reboot requirements in GUI\n\nv7.2:\n\tmore fixes for install failure GUI lockups\n\tfixed: repeating \"checking\" messages (and the assocated calls to setup scripts)\n\nv7.1:\n\tfixed: install failure locks up GUI\n\tcheck for and report patching errors\nv7.0:\n\tfixed: Package editor menu sometimes locks up with Download, etc grayed out\n\tfixed: file set error uninstalls package without prompting for action\n\tfixed: running setup script from inside the package directory fails\n\t\te.g., cd /data/GuiMods; ./setup\n\tadd package dependencies, conflict detection and resolution\n\tadded file system check and status to PackageManager without installing the package\n\t\tso incomplete file set displayes on the GUI without an install attempt\n\tfixed: RemoteGPIO hangs during install\n\tadd RemoteGPIO to default package list\n\timproved reporting of errors in Package editor\n\t\tmost issues are now identified BEFORE an install\n\t\treport no file set and incomplete file set separately\n\t\t\tno file set allows install, incomplete does not\nv6.13: (betas only - never released)\n\tneeded to skip to 6.13 so HelperFiles selection would always work \n\nv6.12: (betas only - never released)\n\nv6.11: skipped\n\nv6.10:\n\tenable auto download and install after package add\n\t\tpreviously, packagew would not auto install if uninstalled manually\n\nv6.9:\n\tfixed: file uninstall fails if extracted from setup script\n\tremoved PackageManager download delays\n\nv6.8:\n\tfixed: PackageManager won't download if package directory doesn't exist\n\t\tbug created in or about v6.5\n\nv6.7:\n\tfixed: GUI white screen for versions prior to v3.00\n\nv6.6:\n\tfixed: PackageManager hangs on remove\n\tfixed: PackageManager not setting no file set for incomplete fs\n\nv6.5:\n\tfixed: package download failure not always reported on the GUI\n\nv6.4:\n\tfixed: setup scripts fail if no fileList file\n\tupdatePacakge: fixed problems related to incompatible versions\n\tupdatePackage: fixed: helper resources updated even if there were no changes\n\nv6.3:\n\tfixed: removing active pachage freezes PackageManager\n\nv6.2:\n\tfixed: repeaded PackageManager install attempts when failure occurs\n\nv6.1:\n\tfixed: white screen for VenusOs prior to v3.00\n\nv6.0:\n\tPackageManager: clear no file set flag when stored version changes\n\tadd automated install, uninstall based on file, services and dBuse Settings lists\n\tmoved package reinstall after Venus OS update to PackageManager\n\t\tonly SetupHelper installed by reinstallMods now\n\tthe unix patch facility is available for modifying replacement files\n\t\tthis may not work for version-dependenet files however\n\t\"helper resources\" can now reside in the package directory in addition to\n\t\tSetupHelper. The code checks for the newest copy and uses that\n\nv5.18\n\tfixed: installs fail on CCGX (resize2fs failure)\n\nv5.17:\n\tfixed: root not always resized after firmware update\n\nv5.16:\n\tfixed: white/black screen on first boot after firmware update\n\tincorporate changes for GUI v1 and gui-v2 selection,\n\t\tmainly to prevent package install if GUI v1 is needed and missing\n\nv5.15:\n\tfixed: PackageManager isn't in menus after v5.14 install\n\tupdateFileSets: fixed: NO_REPLACEMENT in existing file sets that should link to other sets\n\nv5.14:\n\tv5.13 did not include blind install\n\nv5.13:\n\tfixed: GUI not restarted in v3.20~26\n\tfixed: COMPLETE flag not set when creating file new file set\n\t\tthis isn't critical, just slows down installs because file set must be rechecked\n\tupdateFileSets: fixed: USE_ORIGINAL not updated proerly\n\tfixed: typo in package manager backup/restore menu\n\nv5.12:\n\tadd support for gui-v2\n\tdocuments and screen shots moved to a public DropBox:\n\t\thttps://www.dropbox.com/scl/fo/bx5aftvgrqq0vp060mwip/h?rlkey=k28c2i49fjfpcyjfsuldwp159&dl=0\n\nv5.11:\n\tcheck for room on file systems before allowing install\n\tresize root partition before installing a package\n\t\tthe above issues are critical to avoid bricking systems !!!!!!\n\tupdateFileSets:\n\t\tbetter error checking and more status while running long loops\n\t\ta replacement file and USE_ORIGINAL flag was incorrectly allowed\n\t\t\tthe replacement file has priority during package install so\n\t\t\tthis was not a severe issue, but USE_ORIGINAL is now removed in this case\n\nv5.10:\n\tupdateFileSets: check for errors before moving version-independent files\n\nv5.9:\n\tupdateFileSets fixed: creating symlinks in new version sometimes fails\n\nv5.8:\n\tfixed bugs in updateFileSets introduced in v5.7\n\nv5.7:\n\tupdateFileSets: make changes to the copy of the package\n\t\tinstead of to the main package directory\n\t\tso package is not updated until changes are accepted\n\tupdateFileSets: check for version-independent files in file sets\n\t\tand move them from the file sets to FileSets/\n\t\tversion-independent files are those that do not havea stock file\n\t\tand have only one real file in all file sets (other file sets have links)\n\nv5.6:\n\tfixed: version number segments starting with 0 interperted as octal\n\tupdateFileSets: added package backup / restore\n\tchanged status messages to: no file set for vxx.yy~zz\n\nv5.5:\n\tfixed: status text in Active packages menu is black - should be white\n\tupdateFileSets: fixed: reported no package errors when there were some\n\nv5.3 / v5.4:\n\tupdateFileSets: fixed: stale symlinks not removed\n\tupdateFileSets: remove existing file sets that only contain sym links\n\t\tand are not in the stock version list\n\tupdateFileSets: flag file sets that only contain sym links\n\t\tthis aids managing file sets\n\tupdateFileSets: add progress indication to differentiate from a hung app\n\t\tchecks can now take significant amount of time\n\nv5.1/5.2:\n\tremoved original file symbolic links\n\t\tthey are not needed (even for older verisons of SetupHelper)\n\nv5.0:\n\t_checkFileSets in CommonResources:\n\t\tif file set exists and contains the COMPLETE flag file, skip all checks\n\t\totherwise, proceed with file set validation\n\t\tor attempt to create one for the current Venus OS version\n\tupdateFileSets now fills in all file sets with symlinks\n\t\tso that the install does not have to search for a matching original\n\t\tthe search for a match has reportedly failed in a few cases for unknown reasons\n\nv4.43:\n\tfixed: PackageManager crashes when reinitializing database\n\tfixed stuck Package Manager status messages\n\nv4.42:\n\tfixed: updated services are not always restarted\n\tadd ExtTransferSwitch to default package list\n\nv4.41:\n\tadded support for dark mode (thanks mr-manual)\n\nv4.40:\n\tfixed bug in updateFileSets that resulted in no replacement file errors\n\t\tan incorrect not a released version warning\n\nv4.39:\n\tupdateFileSets now allows creating file sets for only released versions\n\tinstallService in ServiceResources now supports multiple services in package\n\tfixed: install button in package edit menu sometimes drawn in wrong posisition\n\nv4.38:\n\tfixed: v4.37 overwrote settingsOptions and added logs.zip to /dataw\n\nv4.37:\n\tadd settings backup to local storage (/data/settingsBackup)\n\nv4.36:\n\tfixed: crash if version file contains a zero length string (not counting white space)\n\tsplit add stored packages processing to reduce execution time in main loop\n\tenhanced Git Hub version updates:\n\t\tlonger background updtate time (10 minutes vs 1 minute)\n\t\tcomplete refresh when entering Active packages menu\n\nv4.35:\n\tfixed: package edit menu buttons are not active after v4.34 update\n\nv4.34:\n\tdisable GitHub version updates if automatic downloads are off\n\t\teliminates internet traffic needed to retrieve version\n\tremove ._ files from blind install archives\n\tadded BatteryAggregator (pulquero) to default packages\n\tupdateFileSets now place files at highest possible version\n\t\tmakes searches faster and makes removing old file sets easier\n\tcheckFileSets now searches version list from highest version to lowest\n\t\tto improve file set creation speed\n\nv4.33:\n\tfixed: menu items not being hidden\n\tadded all logs to Settings backup\n\tadded optional Venus OS firmware update to blindUninstall\n\nv4.32:\n\tadd some packages to the default list\n\nv4.31:\n\tfixed: PackageManager edit menus not working\n\nv4.30:\n\tfixed: can't select many items in PackageManager menus\n\tadd Cerbo SGX platform\n\tadd ExtTransferSwitch to default package list\n\tadd FroniusSmartmeter to default package list\n\tadd dbus-i2c to default package list\n\tadd DCSystemAggregator to default package list\n\tadd gatt-exec-server to default package list\n\nv4.28/v4.29:\n\tmake SetupHelper independent of Venus OS version\n\nv4.27:\n\tfixed 20 MB size for blind isntall archive\n\nv4.25:\n\tfixed: downloads triggered from the PackageManager edit menu\n\t\tare sometimes are delayed by automatic download checks\n\tfixed: fast download sometimes ends before all packages are checked\n\tremoved Fast, then Normal download mode\n\t\tfast downloads are now automatic when enabling downloads\n\nv4.24:\n\tfixed: incompatible version check somtimes fails\n\nv4.23:\n\tadded Cerbo tanks and temps backup/restore\nv4.22:\n\tfixed: reinstallMods does not reboot or restart the GUI if needed\n\tincluded detailed description / help creating setup scripts and file sets\n\nv4.21:\n\tadd missing settings to backup/restore\nv4.20:\n\tadded support for v2.90~22\n\n\nv4.19:\n\tfixed: after a blind install rcS.local did not get updated\n\toptimized reinstallMods - wait for dbus only if script needs to run\n\nv4.18:\n\tcheck versions before installing a package from removable media\n\tcheck versions before transferring a package from removable media\n\tthese prevent a package with ONE_TIME_INSTALL set from installing over and over agin\n\t\tif the removable media is left in place and the system rebooted\n\tadditional fixes for reinstall not working after an OS update\n\nv4.17:\n\tfixed: reinstall not working after OS update\n\tadded AUTO_INSTALL_PACKAGES flag to /data\n\t\tthis flag is easier to build into an archive than the one on removable media\n\t\tbut is removed following the auto install to prevent repeats\n\tadded AUTO_INSTALL flag in each package\n\t\tthis overides the user auto install preferece\n\tdropped support for Venus OS v2.4x and 2.5x\n\nv4.16:\n\tfixed: white screen in Venus v2.73 and earlier\n\nv4.15:\n\treleased - no changes\n\nv4.15~7:\n\tadded delays in install service so things get initialized properly\n\nv4.15~6:\n\tadded blind UNINSTALL via a special venus-data.tar.gz file\n\t\tsee instructions in the ReadMe\n\nv4.14~5:\n\tadded PackageManager persistent storage initialize\n\t\tBoth the INITIALZE_PACKAGE_MANAGER flag file on removable media\n\t\tand a menu item has been added that will trigger the \n\t\tPackageManager dbus Setting storageto be initialized,\n\t\tthen PackageManager restarted.\n\t\tThe storage is then rebuilt when PackageManager starts back up.\n\tadded UNNSTALL_ALL_PACKAGES removable media flag\n\t\tIf this file is found on removable media, PackageManager\n\t\twill UNINSTALL ALL packages including SetupHelper\n\tthese additions help recover systems without a user interface to factory conditions,\n\tincluding a blank or unresponsive GUI\n\nv4.14~4:\n\tupdated ReadMe\n\tfixed: auto eject occured on manual settings restore\n\t\tshould be just AUTOMATIC restores\n\nv4.14~3:\n\tadd auto eject\n\tfixed: couldn't backup or restore settings\n\nv4.14~2:\n\tsettings restore now creates missing parameters\n\trewrote blind install to use the pre/post hooks for v2.90\n\t\tblind install still works with prior Venus OS versions\n\tadded AUTO_INSTALL_PACKAGES flag file on removable media\n\t\tfunctions same as enabling auto install in PackageManager menu\n\tadded support for new /service mechanisms in v2.90\n\nv4.14~1:\n\tadd settings auto restore if SETTINGS_AUTO_RESTORE flag file exists on removable media\n\nv4.13:\n\tadd logs as part of settings backup\n\nv4.12:\n\tadded checks for file set errors before attempting auto install\n\nv4.11:\n\tadded support for Venus OS v2.90~3 firmware\n\nv4.9/10:\n\tadded dbus Settings cleanup to remove invalid packages\n\nv4.8:\n\tfixed: blind install fails if stored SetupHelper version is newer than archive version\n\t\tbut SetupHelper not currently installed.\n\nv4.7:\n\tfixed: another bug matching original files from released and large verisons\n\nv4.6:\n\tfixed: released version file set skipped for versions still in beta\n\t\tthis typically only happens for a large version\n\t\tafter the small version is released\n\nv4.5:\n\tforgot to update blind install files\n\nv4.4:\n\tadded v2.80 - no functional changes\n\nv4.3:\n\tinclude package setup script options in settings backup/restore\n\nv4.2:\n\tfixed: intermittent crash on initialization\n\tfixed: PackageManager doesn't always start after installing SetupHelper\n\t\twith the service overlay\n\t\twhen uninstalling then installing SetupHelper, PackageManager didn't start\n\tfixed: field reports of package with no name in active package list\n\t\tremove any such packages during initialization\n\nv4.1:\n\tfixed: packageManager crash when moving old DO_NOT ... flags to setupOptions\n\tfixed: white screen on v2.8~33-large-24\n\nv4.0:\n\tbeta test period ended\n\tadded running version to PackageManager sign-on\n\nv4.0~38:\n\tfixed: backup/restore hangs\n\nv4.0~37:\n\tchanged the blind install process to minimize issues if venus-data.tgz is left mounted\n\tSetupHelper now unpacks to /data/SetupHelper-blind,\n\t\tthen is moved to /data/SetupHelper and the setup script run\n\t\tONLY IF it is a newer version\n\nv4.0~36:\n\tblind install was't updated for ~35 !!!\n\nv4.0~35:\n\treinstall now compares installed and package versions and installs if they differ\n\tpreviously, booting to the alternate installed Venus version would not trigger\n\t\ta package reinstall, possibly resulting in problems or at least out of date packages\n\nv4.0~34:\n\tadded image overlays to backup/restore\n\t\tthis includes custom logos for Mobile and Tile overviews\n\nv4.0~33:\n\tfixed: PackageManager doesn't run on Venus versions prior to v2.80~10 (Python 2).\n\nv4.0~32:\n\tfixed: version numbers not in the Victron format would crash PackageManager\n\t\talso accommodate a other version string formats:\n\t\tvX.Y.Z, vX.YdZ, vX.YaZ, vX.YbZ\n\nv4.0~31:\n\tfixed crashes and bugs that prevented initial install on a system\n\t\tthat has no packages yet\n\nv4.0~30:\n\tadded settings backup/restore\n\t\tnote this is NOT the Victron mechanism\n\t\trather, it extracts SOME of the Settings parameters\n\t\tand writes these to a file\n\t\tcare was taken to save/restore only those parameters that\n\t\t\tshould not cause conflicts\n\t\tWhen Victron releases their mechanism, this one will be removed\n\nv4.0~26:\n\tfixed: Large features not appearing\n\tfixed: typo in 123SmartBMS-Venus in defaultPackageList\n\nv4.0~25:\n\thandle nonexistant package directory\n\tpull GitHub user/branch from package directory file\n\toptimize AddStoredPackages so it can run all the time\n\t\t(some updates were being missed)\n\tchanged default package name: smartbms-venus to 123SmartBMS-Venus\n\nv4.0~24:\n\tadd Reboot/GUI restart button to Package Manager main menu\n\t\tto address deferred operations (\"Later\")\n\nv4.0~23:\n\tfixed: GUI restart not happening after auto install\n\tfixed: GUI restart notificaiton in menu not cleared after GUI restart\nv4.0~22: not used\nv4.0~21:\n\tfixed: adding package didn't carry over GitHub user and branch\n\nv4.0~20:\n\trearranged package editor menus\n\t\tPackage version list -> Active packages\n\t\t\ttapping on an entry leads to Package editor\n\t\tadded Inactive packages\n\t\t\twhich shows only packages that are not on the system yet (or manually removed)\n\t\t\ttapping on entry leads to Add package menu\n\t\tadded separate Add package menu\n\t\tPackage Editor eliminated from main menu (access through Active packages only)\n\tdefault packages are no longer automatically added to the active package list\n\nv4.0~19:\n\tfixed: venus-data.tar.gz in v2.80~18 didn't include PackageManager.py\n\nv4.0~18:\n\tfixed: packages auto add/install when PackageManager is restarted\n\t\teven if REMOVED / DO_NOT_AUTO_INSTALL was set\n\tmore work on GUI getting \"stuck\"\n\tbogus \"unpack tar from GitHub failed\" message - package downloads properly\n\tmajor change to thread structure to make operations more responsive\n\t\tand to minimize CPU consumption when idle\n\nv4.0~17:\n\tfixed: package editor status did not always show package name\n\tfixed?: occationally, Package Editor appears to get stuck when a PackageManager\n\t\t\taction completes.\n\t\tA missed property update from the dbus paramter may be yhe cause\n\t\tSet a timer to refresh properties in the GUI\n\nv4.0~16:\n\tfixed: packages auto auto-adding following manual removal\n\nv4.0~15:\n\tfixed: auto download not working\n\tmoved dbus settings for PackageManager to /Settings/PackageManager\n\t\tfrom /Settings/PackageMonitor\n\tremove dbusSettings when package is removed\n\t\tpreviously these were left in place\n\tchanged menu items and titles to conform to Victron standards:\n\t\tonly firt word capitalized\n\nv4.0~14:\n\tfixed: selecting \"Now\" in GUI when reboot needed does nothing\n\tfixed: blind install did not work\n\nv4.0~13:\n\tadded support for Venus OS v2.80~33-large-24\n\nv4.0~12:\n\tfixed: manually uninstalled packages would reinstall immediately\n\t\tif auto install was on\n\taccommodate Python 2.7 for Venus OS prior to v2.80~10\n\tfixed: a setup script run failure was not handled properly and\n\t\tcaused the install thread to hang\nv4.0~11:\n\tfixed bug that caused GUI to restart repeatedly if package was not\n\t\tcompatible with the current Venus version\n\t\te.g., a file set error\n\tmajor rewrite to PackageManager download code\n\tchanges to GUI:\n\t\tAdd Package -> New Package\n\t\tmoved SetpHelper uninstall warning to status\n\t\tmoved action confirmaiton message to status,\n\t\t\tConfirm ... button now reads Proceed\n\t\tremoved \"can't remove\" ... message\nv4.0~10:\n\tGitHub downloads and SD/USB transfers now scan the entire directory tree\n\t\tsearching for a package directory. This was done because of the 123 smartBMS\n\t\tarchive directory structure but there are other issues preventing integration\n\t\twith PackageManger.\n\tFixed bug that showed a blank status line with the OK button after a download\n\t\tRhe OK button no longer appears and Package Editor menu returns\n\t\tto the \"navigation\" mode\n\nv4.0~9:\n\tfixed bug with firstCompatibleVersion\n\tadded try: / except: around all subprocess.run calls\n\t\tso if the call fails, the program continues to run\n\nv4.0~8:\n\trefresh GitHub version prior to download checks\n\t\tnew upadates to GitHub could be missed\n\treduced GitHub vesion refres delay\n\ttouching a row in Package Version List menu\n\t\tleads to Package Editor menu\n\t\tand < will return to the version list\n\nv4.0~5 - 7:\n\tdownload bug fixes\nv4.0~4:\n\tadd Package Manager & GUI\n\tadd setup script return codes for above\n\tadd optionsRequired flag file (VeCanSetup is only package that needs this now)\n\n\tadd platform and version checks to CommonResources\n\tadd install opiton to CommonResources\n\tbetter support installs without command line\n\t### TBD remove logging to package log files\n\n\timprove adding packages from SD/USB\n\n\tsplit auto download and auto install\n\n\n"
  },
  {
    "path": "defaultPackageList",
    "content": "# the DEFAULT list of packages managed by SetupHelper\n# actual list is based on what is stored on the system\n# this list assists in adding new packages\n# lines beginning with # are ignored and can be used\n#       to remove a package from auto and manual updates\n#       or as comments\n# blank lines are ignored\n# incomplete lines are ignored\n\n# Package             GitHubUser    Tag/branch/version\nSetupHelper           kwindrem      latest\nGuiMods               kwindrem      latest\nShutdownMonitor       kwindrem      latest\nVeCanSetup            kwindrem      latest\nRpiDisplaySetup       kwindrem      latest\nRpiGpioSetup          kwindrem      latest\nTailscaleGX           kwindrem      latest\n\n123SmartBMS-Venus     123electric   latest\nRpiTemperature        TimD1981      latest\nBatteryAggregator     pulquero      latest\ndbus-i2c              pulquero      latest\nDCSystemAggregator    pulquero      latest\ngatt-exec-server      pulquero      latest\ndbus-pi               pulquero      latest\nFroniusSmartmeter     SirUli        main\nRemoteGPIO            Lucifer06     main\n"
  },
  {
    "path": "forSetupScript",
    "content": "#### add the following lines to the package's setup script\n\n#### following line incorporates helper resources into this script\nsource \"/data/SetupHelper/HelperResources/IncludeHelpers\"\n#### end of lines to include helper resources\n"
  },
  {
    "path": "genericSetupScript",
    "content": "#!/bin/bash\n\n# this script will install any package that can use\n# the automated install and uninstall mechanisms provided by SetupHelper\n# that is, no custom prompting for command line exection\n#\tand no custom installation such as editing replacement files\n#\n# link the package's setup script to this one:\n#\tln -s /data/SetupHelper/genericSetupScript /data/<package name>/setup\n\n# tell CommonResources to:\n#\tprompt for install/uninstall\n#\tauto install or auto uninstall\n#\tthen exit\n#\tCommonResources will NOT return here !\n\nstandardPromptAndActions='yes'\n\n#### following line incorporates helper resources into this script\nsource \"/data/SetupHelper/HelperResources/IncludeHelpers\"\n#### end of lines to include helper resources\n\n# never returns from CommonResources !\n"
  },
  {
    "path": "gitHubInfo",
    "content": "kwindrem:latest\n"
  },
  {
    "path": "makeVelib_python",
    "content": "#!/bin/bash\n\n\n\n# convert a version string to an integer to make comparisions easier\n#\n#\tNote: copied from VersionResources\n#\t\tbut also includes code to report duplcates not in the VersionResources version\n\nfunction versionStringToNumber ()\n{\n\tlocal version=\"$*\"\n\tlocal numberParts\n\tlocal versionParts\n\tlocal numberParts\n\tlocal otherParts\n\tlocal other\n\tlocal number=0\n\tlocal type='release'\n\n\t# split incoming string into\n\t# an array of numbers: major, minor, prerelease, etc\n\t# and an array of other substrings\n\t# the other array is searched for releasy type strings and the related offest added to the version number\n\t\n\tread -a numberParts <<< $(echo $version | tr -cs '0-9' ' ')\n\tnumberPartsLength=${#numberParts[@]}\n\tif (( $numberPartsLength == 0 )); then\n\t\tversionNumber=0\n\t\tversionStringToNumberStatus=\"$version: invalid, missing major version\"\n\t\treturn 1\n\tfi\n\tif (( $numberPartsLength >= 2 )); then\n\t\tread -a otherParts <<< $(echo $version | tr -s '0-9' ' ')\n\t\tfor other in ${otherParts[@]}; do\n\t\t\tcase $other in\n\t\t\t\t'b' | '~')\n\t\t\t\t\ttype='beta'\n\t\t\t\t\t(( number += 60000 ))\n\t\t\t\t\tbreak ;;\n\t\t\t\t'a')\n\t\t\t\t\ttype='alpha'\n\t\t\t\t\t(( number += 30000 ))\n\t\t\t\t\tbreak ;;\n\t\t\t\t'd')\n\t\t\t\t\ttype='develop'\n\t\t\t\t\tbreak ;;\n\t\t\tesac\n\t\tdone\n\tfi\n\n\t# if release all parts contribute to the main version number\n\t#\tand offset is greater than all prerelease versions\n\tif [ \"$type\" == \"release\" ] ; then\n\t\t(( number += 90000 ))\n\t# if pre-release, last part will be the pre release part\n\t#\tand others part will be part the main version number\n\telse\n\t\t(( numberPartsLength-- ))\n\t\t(( number += 10#${numberParts[$numberPartsLength]} ))\n\tfi\n\t# include core version number\n\t(( number += 10#${numberParts[0]} * 10000000000000 ))\n\tif (( numberPartsLength >= 2)); then\n\t\t(( number += 10#${numberParts[1]} * 1000000000 ))\n\tfi\n\tif (( numberPartsLength >= 3)); then\n\t\t(( number += 10#${numberParts[2]} * 100000 ))\n\tfi\n\n\tversionNumber=$number\n\tversionStringToNumberStatus=\"$version:$number $type\"\n\treturn 0\n}\n\n\ntotalErrors=0\ntotalWarnings=0\npackageErrors=0\npackageWarnings=0\n\noutputtingProgress=false\n\n\nfunction logMessage ()\n{\n\tif $outputtingProgress ; then\n\t\tclearProgress\n\tfi\n    echo \"$*\"\n    if [[ \"$*\" == \"ERROR\"* ]]; then\n        ((totalErrors++))\n        ((packageErrors++))\n    elif [[ \"$*\" == \"WARNING\"* ]]; then\n        ((totalWarnings++))\n        ((packageWarnings++))\n    fi\n}\n\nfunction outputProgressTick ()\n{\n\tif ! $outputtingProgress ; then\n\t\techo -en \"$beginProgressString\"\n\tfi\n\techo -en \"$1\"\n\toutputtingProgress=true\n}\n\nfunction clearProgress ()\n{\n\t# start a new line if outputting ticks\n\tif $outputtingProgress; then\n\t\techo\n\t\t# echo -ne \"\\r\\033[2K\" #### erase line\n\tfi\n\toutputtingProgress=false\n}\n\nbeginProgressString=\"\"\n\nfunction beginProgress ()\n{\n\t# erase the line but stay on it\n\tif $outputtingProgress ; then\n\t\tclearProgress\n\tfi\n\tif [ ! -z \"$1\" ]; then\n\t\tbeginProgressString=\"$1 \"\n\t\techo -en \"$beginProgressString\"\n\t\t\n\t\toutputtingProgress=true\n\tfi\n}\n\n\n\n#### script code begins here\n\n# attempt to locate SharedUtilities based on the location of this script\n#\t(it is assumed to be in the SetupHelper directory)\n# also sets the package root directory based on this also\n# and also the stock files base directory\n#\n# if these are not correct, edit the lines below to set the appropriate values\n\nscriptDir=\"$( cd $(dirname \"$0\") >/dev/null 2>&1 ; /bin/pwd -P )\"\npackageRoot=\"$( dirname $scriptDir )\"\nstockFiles=\"$packageRoot/StockVenusOsFiles\"\npythonLibDir=\"opt/victronenergy/dbus-systemcalc-py/ext/velib_python\"\nveLibFiles=( vedbus.py dbusmonitor.py settingsdevice.py ve_utils.py )\n\n#### set these as appropriate to your system if the values set above are not correct\n#### packageRoot=FILL_THIS_IN_AND_UNCOMMENT_LINE\n#### stockFiles=FILL_THIS_IN_AND_UNCOMMENT_LINE\n\nif [ ! -e \"$packageRoot\" ]; then\n\techo \"unable to locate package root - can't continue\"\n\texit\nelif [ ! -e \"$stockFiles\" ]; then\n\techo \"unable to locate stock files - can't continue\"\n\texit\nfi\n\n\n# make the version list from the directories in stock files\n# version lists are sorted so the most recent version is first\ntempList=()\nstockVersionList=($(ls -d \"$stockFiles\"/v[0-9]* 2> /dev/null))\nfor entry in ${stockVersionList[@]} ; do\n    version=$(basename $entry)\n    versionFile=\"$stockFiles/$version/opt/victronenergy/version\"\n\tif [ -f \"$versionFile\" ]; then\n\t\trealVersion=$(cat \"$versionFile\" | head -n 1)\n\telse\n        logMessage \"ERROR version file missing from stock files $version - can't continue\"\n        exit\n\tfi\n\n    if [ $version != $realVersion ]; then\n        logMessage \"ERROR $version name does not mactch Venus $realVersion - can't continue\"\n        exit\n    fi\n\tif versionStringToNumber $version ; then\n\t\ttempList+=(\"$version:$versionNumber\")\n\telse\n\t\tlogMessage \"ERROR invalid version $versionStringToNumberStatus - not added to list\"\n\tfi\ndone\nstockVersionList=( $(echo ${tempList[@]} | tr ' ' '\\n' | sort -t ':' -r -n -k 2 | uniq ) )\nstockVersionListLength=${#stockVersionList[@]}\n\nif (( stockVersionListLength < 2 )); then\n\tlogMessage \"fewer than 2 versions - nothing to compare\"\n\texit\nfi\n\nif [ -e \"$scriptDir/velib_python\" ]; then\n\trm -rf \"$scriptDir/velib_python\"\nfi\nmkdir -p \"$scriptDir/velib_python\"\n\nfor (( i1 = 0; i1 < $stockVersionListLength; i1++ )); do\n\tnewVersion=false\n\tIFS=':' read version versionNumber <<< \"${stockVersionList[$i1]}\"\n\n\tif (( i1  == 0 )); then\n\t\tnewVersion=true\n\telse\n\t\tfor file in ${veLibFiles[@]} ; do\n\t\t\tfile1=\"$stockFiles/$version/$pythonLibDir/$file\"\n\t\t\tfile2=\"$stockFiles/$previousVersion/$pythonLibDir/$file\"\n\t\t\tif ! cmp -s \"$file1\" \"$file2\" > /dev/null ; then\n\t\t\t\tlogMessage \"    $file $previousVersion $version differ\"\n\t\t\t\tnewVersion=true\n\t\t\tfi\n\t\tdone\n\tfi\n\n\tif $newVersion ; then\n\t\tif (( i1 == 0 ));then\n\t\t\tvelibDir=\"$scriptDir/velib_python/latest\"\n\t\t\tprevVelibDir=\"$scriptDir/velib_python/latest\"\n\t\telse\n\t\t\tvelibDir=\"$scriptDir/velib_python/$version\"\n\t\tfi\n\t\tmkdir \"$velibDir\"\n\t\tlogMessage \"new velib_python version $version\"\n\t\tfor file in ${veLibFiles[@]} ; do\n\t\t\tfile1=\"$stockFiles/$version/$pythonLibDir/$file\"\n\t\t\tfile2=\"$velibDir/$file\"\n\t\t\tcp -f \"$file1\" \"$file2\"\n\t\tdone\n\t\tnewVersion=false\n\t\tpreviousVersion=$version\n\t\tprevVelibDir=\"$velibDir\"\n\tfi\n\techo $version > \"$prevVelibDir/oldestVersion\"\ndone\n"
  },
  {
    "path": "rcS.local",
    "content": "#!/bin/bash\n\n# SetupHelper reinstall\nif [ -f /data/SetupHelper/reinstallMods ]; then\n\tnohup /data/SetupHelper/reinstallMods > /dev/null &\nfi\t#end SetupHelper reinstall\n"
  },
  {
    "path": "reinstallMods",
    "content": "#!/bin/sh\n\n# this script is called from /data/rcS.local during system boot\n# it checks to see the PackageManager service is installed and if not,\n#\twill install ONLY the PackageManager service\n#\n# the REINSTALL_PACKAGES flag file is then set so that\n#\twhen PackageManger runs, it will do boot-time reinstall checks for all packages\n#\tPackageManager then clears this flag when all install checks have been made\n\nscriptDir=\"$( cd \"$(dirname $0)\" >/dev/null 2>&1 ; /bin/pwd -P )\"\nhelperResourcesDir=\"$scriptDir/HelperResources\"\nsource \"$helperResourcesDir/EssentialResources\"\nsource \"$helperResourcesDir/ServiceResources\"\n\n# disable outputting log messages to console\nlogToConsole=false\n\nif [ -f \"$setupOptionsDir/DO_NOT_AUTO_INSTALL\" ]; then\n\tlogMessage \"CRITICAL: SetupHelper was manually uninstalled therefore it was not reinstalled\"\n\tlogMessage \"    other packages will NOT BE REINSTALLED either !\"\n\n\t# remove lines from rcS.local that call reinstallMods so this doesn't happen repeatadly\n\tsed -i -e \"/# SetupHelper reinstall/,/fi/d\" \"$rcLocal\"\n\n# install PackageManager service\nelse\n\t# installing the PackageManager service requires remounting root R/W\n\tupdateRootToReadWrite\n\n\tif ! $installFailed ; then\n\t\t# install PackageManager service if not yet installed\n\t\tif ! [ -e \"$serviceDir/PackageManager\" ]; then\n\t\t\tlogMessage \"installing PackageManager service - PackageManager will reinstall all packages\"\n\t\t\tinstallService PackageManager\n\t\tfi\n\tfi\n\tif ! $installFailed ; then\n\t\ttouch \"/etc/venus/REINSTALL_PACKAGES\"\n\telse\n\t\tlogMessage \"reinstallMods not completed - packages will not be reinstalled\"\n\tfi\nfi\n\n\n"
  },
  {
    "path": "services/PackageManager/log/run",
    "content": "#!/bin/sh\nexec multilog t s25000 n4 /var/log/PackageManager\n\n"
  },
  {
    "path": "services/PackageManager/run",
    "content": "#!/bin/sh\nexec 2>&1\nexec /data/SetupHelper/PackageManager.py\n\n"
  },
  {
    "path": "settingsList",
    "content": "/Settings/Alarm/Audible\n/Settings/Alarm/System/GridLost\n/Settings/Alarm/Vebus/HighDcCurrent\n/Settings/Alarm/Vebus/HighDcRipple\n/Settings/Alarm/Vebus/HighDcVoltage\n/Settings/Alarm/Vebus/HighTemperature\n/Settings/Alarm/Vebus/InverterOverload\n/Settings/Alarm/Vebus/LowBattery\n/Settings/Alarm/Vebus/TemperatureSenseError\n/Settings/Alarm/Vebus/VeBusError\n/Settings/Alarm/Vebus/VoltageSenseError\n/Settings/Ble/Service/Pincode\n/Settings/CGwacs/AcPowerSetPoint\n/Settings/CGwacs/BatteryLife/DischargedTime\n/Settings/CGwacs/BatteryLife/Flags\n/Settings/CGwacs/BatteryLife/MinimumSocLimit\n/Settings/CGwacs/BatteryLife/Schedule/Charge/0/Day\n/Settings/CGwacs/BatteryLife/Schedule/Charge/0/Duration\n/Settings/CGwacs/BatteryLife/Schedule/Charge/0/Soc\n/Settings/CGwacs/BatteryLife/Schedule/Charge/0/Start\n/Settings/CGwacs/BatteryLife/Schedule/Charge/1/Day\n/Settings/CGwacs/BatteryLife/Schedule/Charge/1/Duration\n/Settings/CGwacs/BatteryLife/Schedule/Charge/1/Soc\n/Settings/CGwacs/BatteryLife/Schedule/Charge/1/Start\n/Settings/CGwacs/BatteryLife/Schedule/Charge/2/Day\n/Settings/CGwacs/BatteryLife/Schedule/Charge/2/Duration\n/Settings/CGwacs/BatteryLife/Schedule/Charge/2/Soc\n/Settings/CGwacs/BatteryLife/Schedule/Charge/2/Start\n/Settings/CGwacs/BatteryLife/Schedule/Charge/3/Day\n/Settings/CGwacs/BatteryLife/Schedule/Charge/3/Duration\n/Settings/CGwacs/BatteryLife/Schedule/Charge/3/Soc\n/Settings/CGwacs/BatteryLife/Schedule/Charge/3/Start\n/Settings/CGwacs/BatteryLife/Schedule/Charge/4/Day\n/Settings/CGwacs/BatteryLife/Schedule/Charge/4/Duration\n/Settings/CGwacs/BatteryLife/Schedule/Charge/4/Soc\n/Settings/CGwacs/BatteryLife/Schedule/Charge/4/Start\n/Settings/CGwacs/BatteryLife/SocLimit\n/Settings/CGwacs/BatteryLife/State\n/Settings/CGwacs/MaxChargePercentage\n/Settings/CGwacs/MaxChargePower\n/Settings/CGwacs/MaxDischargePercentage\n/Settings/CGwacs/MaxDischargePower\n/Settings/CGwacs/MaxFeedInPower\n/Settings/CGwacs/OvervoltageFeedIn\n/Settings/CGwacs/PreventFeedback\n/Settings/CGwacs/RunWithoutGridMeter\n/Settings/CanBms/SocketcanCan0/ProductId\n/Settings/Canbus/can0/Profile\n/Settings/Canbus/can1/Profile\n/Settings/Devices/vebus_ttyUSB0/CustomName\n/Settings/Devices/vebus_ttyUSB1/CustomName\n/Settings/DigitalInput/1/AlarmSetting\n/Settings/DigitalInput/1/Count\n/Settings/DigitalInput/1/CustomName\n/Settings/DigitalInput/1/InvertAlarm\n/Settings/DigitalInput/1/InvertTranslation\n/Settings/DigitalInput/1/Multiplier\n/Settings/DigitalInput/1/Type\n/Settings/DigitalInput/2/AlarmSetting\n/Settings/DigitalInput/2/Count\n/Settings/DigitalInput/2/CustomName\n/Settings/DigitalInput/2/InvertAlarm\n/Settings/DigitalInput/2/InvertTranslation\n/Settings/DigitalInput/2/Multiplier\n/Settings/DigitalInput/2/Type\n/Settings/DigitalInput/3/AlarmSetting\n/Settings/DigitalInput/3/Count\n/Settings/DigitalInput/3/CustomName\n/Settings/DigitalInput/3/InvertAlarm\n/Settings/DigitalInput/3/InvertTranslation\n/Settings/DigitalInput/3/Multiplier\n/Settings/DigitalInput/3/Type\n/Settings/DigitalInput/4/AlarmSetting\n/Settings/DigitalInput/4/Count\n/Settings/DigitalInput/4/CustomName\n/Settings/DigitalInput/4/InvertAlarm\n/Settings/DigitalInput/4/InvertTranslation\n/Settings/DigitalInput/4/Multiplier\n/Settings/DigitalInput/4/Type\n/Settings/DigitalInput/5/AlarmSetting\n/Settings/DigitalInput/5/Count\n/Settings/DigitalInput/5/CustomName\n/Settings/DigitalInput/5/InvertAlarm\n/Settings/DigitalInput/5/InvertTranslation\n/Settings/DigitalInput/5/Multiplier\n/Settings/DigitalInput/5/Type\n/Settings/DigitalInput/6/AlarmSetting\n/Settings/DigitalInput/6/Count\n/Settings/DigitalInput/6/CustomName\n/Settings/DigitalInput/6/InvertAlarm\n/Settings/DigitalInput/6/InvertTranslation\n/Settings/DigitalInput/6/Multiplier\n/Settings/DigitalInput/6/Type\n/Settings/FischerPanda0/AcLoad/Enabled\n/Settings/FischerPanda0/AcLoad/Measurement\n/Settings/FischerPanda0/AcLoad/QuietHoursStartValue\n/Settings/FischerPanda0/AcLoad/QuietHoursStopValue\n/Settings/FischerPanda0/AcLoad/StartTimer\n/Settings/FischerPanda0/AcLoad/StartValue\n/Settings/FischerPanda0/AcLoad/StopTimer\n/Settings/FischerPanda0/AcLoad/StopValue\n/Settings/FischerPanda0/Alarms/NoGeneratorAtAcIn\n/Settings/FischerPanda0/AutoStartEnabled\n/Settings/FischerPanda0/BatteryCurrent/Enabled\n/Settings/FischerPanda0/BatteryCurrent/QuietHoursStartValue\n/Settings/FischerPanda0/BatteryCurrent/QuietHoursStopValue\n/Settings/FischerPanda0/BatteryCurrent/StartTimer\n/Settings/FischerPanda0/BatteryCurrent/StartValue\n/Settings/FischerPanda0/BatteryCurrent/StopTimer\n/Settings/FischerPanda0/BatteryCurrent/StopValue\n/Settings/FischerPanda0/BatteryService\n/Settings/FischerPanda0/BatteryVoltage/Enabled\n/Settings/FischerPanda0/BatteryVoltage/QuietHoursStartValue\n/Settings/FischerPanda0/BatteryVoltage/QuietHoursStopValue\n/Settings/FischerPanda0/BatteryVoltage/StartTimer\n/Settings/FischerPanda0/BatteryVoltage/StartValue\n/Settings/FischerPanda0/BatteryVoltage/StopTimer\n/Settings/FischerPanda0/BatteryVoltage/StopValue\n/Settings/FischerPanda0/InverterHighTemp/Enabled\n/Settings/FischerPanda0/InverterHighTemp/StartTimer\n/Settings/FischerPanda0/InverterHighTemp/StopTimer\n/Settings/FischerPanda0/InverterOverload/Enabled\n/Settings/FischerPanda0/InverterOverload/StartTimer\n/Settings/FischerPanda0/InverterOverload/StopTimer\n/Settings/FischerPanda0/MinimumRuntime\n/Settings/FischerPanda0/OnLossCommunication\n/Settings/FischerPanda0/QuietHours/EndTime\n/Settings/FischerPanda0/QuietHours/StartTime\n/Settings/FischerPanda0/Soc/Enabled\n/Settings/FischerPanda0/Soc/QuietHoursStartValue\n/Settings/FischerPanda0/Soc/QuietHoursStopValue\n/Settings/FischerPanda0/Soc/StartValue\n/Settings/FischerPanda0/Soc/StopValue\n/Settings/FischerPanda0/StopWhenAc1Available\n/Settings/FischerPanda0/TestRun/Duration\n/Settings/FischerPanda0/TestRun/Enabled\n/Settings/FischerPanda0/TestRun/Interval\n/Settings/FischerPanda0/TestRun/RunTillBatteryFull\n/Settings/FischerPanda0/TestRun/SkipRuntime\n/Settings/FischerPanda0/TestRun/StartDate\n/Settings/FischerPanda0/TestRun/StartTime\n/Settings/Generator0/AcLoad/Enabled\n/Settings/Generator0/AcLoad/Measurement\n/Settings/Generator0/AcLoad/QuietHoursStartValue\n/Settings/Generator0/AcLoad/QuietHoursStopValue\n/Settings/Generator0/AcLoad/StartTimer\n/Settings/Generator0/AcLoad/StartValue\n/Settings/Generator0/AcLoad/StopTimer\n/Settings/Generator0/AcLoad/StopValue\n/Settings/Generator0/Alarms/NoGeneratorAtAcIn\n/Settings/Generator0/AutoStartEnabled\n/Settings/Generator0/BatteryCurrent/Enabled\n/Settings/Generator0/BatteryCurrent/QuietHoursStartValue\n/Settings/Generator0/BatteryCurrent/QuietHoursStopValue\n/Settings/Generator0/BatteryCurrent/StartTimer\n/Settings/Generator0/BatteryCurrent/StartValue\n/Settings/Generator0/BatteryCurrent/StopTimer\n/Settings/Generator0/BatteryCurrent/StopValue\n/Settings/Generator0/BatteryService\n/Settings/Generator0/BatteryVoltage/Enabled\n/Settings/Generator0/BatteryVoltage/QuietHoursStartValue\n/Settings/Generator0/BatteryVoltage/QuietHoursStopValue\n/Settings/Generator0/BatteryVoltage/StartTimer\n/Settings/Generator0/BatteryVoltage/StartValue\n/Settings/Generator0/BatteryVoltage/StopTimer\n/Settings/Generator0/BatteryVoltage/StopValue\n/Settings/Generator0/InverterHighTemp/Enabled\n/Settings/Generator0/InverterHighTemp/StartTimer\n/Settings/Generator0/InverterHighTemp/StopTimer\n/Settings/Generator0/InverterOverload/Enabled\n/Settings/Generator0/InverterOverload/StartTimer\n/Settings/Generator0/InverterOverload/StopTimer\n/Settings/Generator0/MinimumRuntime\n/Settings/Generator0/OnLossCommunication\n/Settings/Generator0/QuietHours/Enabled\n/Settings/Generator0/QuietHours/EndTime\n/Settings/Generator0/QuietHours/StartTime\n/Settings/Generator0/Soc/Enabled\n/Settings/Generator0/Soc/QuietHoursStartValue\n/Settings/Generator0/Soc/QuietHoursStopValue\n/Settings/Generator0/Soc/StartValue\n/Settings/Generator0/Soc/StopValue\n/Settings/Generator0/StopWhenAc1Available\n/Settings/Generator0/TestRun/Duration\n/Settings/Generator0/TestRun/Enabled\n/Settings/Generator0/TestRun/Interval\n/Settings/Generator0/TestRun/RunTillBatteryFull\n/Settings/Generator0/TestRun/SkipRuntime\n/Settings/Generator0/TestRun/StartDate\n/Settings/Generator0/TestRun/StartTime\n/Settings/Gps/Format\n/Settings/Gps/SpeedUnit\n/Settings/Gui/AutoBrightness\n/Settings/Gui/Brightness\n/Settings/Gui/DisplayOff\n/Settings/Gui/Language\n/Settings/Gui/MobileOverview\n/Settings/Gui/StartWithMenuView\n/Settings/Gui/TanksOverview\n/Settings/GuiMods/AcCurrentLimit/Preset1\n/Settings/GuiMods/AcCurrentLimit/Preset2\n/Settings/GuiMods/AcCurrentLimit/Preset3\n/Settings/GuiMods/AcCurrentLimit/Preset4\n/Settings/GuiMods/CustomDcSystemName\n/Settings/GuiMods/EnhancedFlowCombineLoads\n/Settings/GuiMods/FlowOverview\n/Settings/GuiMods/GaugeLimits/AcOutputMaxPower\n/Settings/GuiMods/GaugeLimits/AcOutputNonCriticalMaxPower\n/Settings/GuiMods/GaugeLimits/AlternatorMaxPower\n/Settings/GuiMods/GaugeLimits/BatteryMaxChargeCurrent\n/Settings/GuiMods/GaugeLimits/BatteryMaxDischargeCurrent\n/Settings/GuiMods/GaugeLimits/CautionPower\n/Settings/GuiMods/GaugeLimits/ContiuousPower\n/Settings/GuiMods/GaugeLimits/DcSystemMaxCharge\n/Settings/GuiMods/GaugeLimits/DcSystemMaxLoad\n/Settings/GuiMods/GaugeLimits/MaxAcChargerPower\n/Settings/GuiMods/GaugeLimits/MaxAlternatorPower\n/Settings/GuiMods/GaugeLimits/MaxChargerPower\n/Settings/GuiMods/GaugeLimits/MaxFeedInPower\n/Settings/GuiMods/GaugeLimits/MaxFuelCellPower\n/Settings/GuiMods/GaugeLimits/MaxWindGenPower\n/Settings/GuiMods/GaugeLimits/PeakPower\n/Settings/GuiMods/GaugeLimits/PvChargerMaxPower\n/Settings/GuiMods/GaugeLimits/PvOnGridMaxPower\n/Settings/GuiMods/GaugeLimits/PvOnOutputMaxPower\n/Settings/GuiMods/MoveSettings\n/Settings/GuiMods/ShortenTankNames\n/Settings/GuiMods/ShowBatteryTempOnFlows\n/Settings/GuiMods/ShowEnhancedFlowLoadsOnInput\n/Settings/GuiMods/ShowEnhancedFlowOverviewTanks\n/Settings/GuiMods/ShowEnhancedFlowOverviewTemps\n/Settings/GuiMods/ShowGauges\n/Settings/GuiMods/ShowInactiveFlowTiles\n/Settings/GuiMods/ShowRelayOverview\n/Settings/GuiMods/ShowTanksTempsDigIn\n/Settings/GuiMods/ShowTileOverview\n/Settings/GuiMods/TemperatureScale\n/Settings/GuiMods/TimeFormat\n/Settings/GuiMods/UseEnhancedFlowOverview\n/Settings/GuiMods/UseEnhancedGridParallelFlowOverview\n/Settings/GuiMods/UseEnhancedMobileOverview\n/Settings/GuiMods/UsedEnhancedGeneratorOverview\n/Settings/PackageManager/AutoInstall\n/Settings/PackageManager/Count\n/Settings/PackageManager/GitHubAutoDownload\n/Settings/Pump0/AutoStartEnabled\n/Settings/Pump0/Mode\n/Settings/Pump0/StartValue\n/Settings/Pump0/StopValue\n/Settings/Pump0/TankService\n/Settings/Relay/0/CustomName\n/Settings/Relay/0/InitialState\n/Settings/Relay/0/Show\n/Settings/Relay/1/CustomName\n/Settings/Relay/1/Function\n/Settings/Relay/1/InitialState\n/Settings/Relay/1/Polarity\n/Settings/Relay/1/Show\n/Settings/Relay/2/CustomName\n/Settings/Relay/2/InitialState\n/Settings/Relay/2/Show\n/Settings/Relay/3/CustomName\n/Settings/Relay/3/InitialState\n/Settings/Relay/3/Show\n/Settings/Relay/4/CustomName\n/Settings/Relay/4/InitialState\n/Settings/Relay/4/Show\n/Settings/Relay/5/CustomName\n/Settings/Relay/5/InitialState\n/Settings/Relay/5/Show\n/Settings/Relay/6/InitialState\n/Settings/Relay/Function\n/Settings/Relay/Polarity\n/Settings/Services/AccessPoint\n/Settings/Services/BleSensors\n/Settings/Services/Bluetooth\n/Settings/Services/Bol\n/Settings/Services/Console\n/Settings/Services/FischerPandaAutoStartStop\n/Settings/Services/Modbus\n/Settings/Services/MqttLocal\n/Settings/Services/MqttLocalInsecure\n/Settings/Services/MqttN2k\n/Settings/Services/MqttVrm\n/Settings/Services/NodeRed\n/Settings/Services/SignalK\n/Settings/ShutdownMonitor/ExternalSwitch\n/Settings/System/AccessLevel\n/Settings/System/AutoUpdate\n/Settings/System/ImageType\n/Settings/System/LogLevel\n/Settings/System/ReleaseType\n/Settings/System/RemoteSupport\n/Settings/System/SSHLocal\n/Settings/System/TimeZone\n/Settings/System/Units/Temperature\n/Settings/System/VncInternet\n/Settings/System/VncLocal\n/Settings/System/VolumeUnit\n/Settings/SystemSetup/AcInput1\n/Settings/SystemSetup/AcInput2\n/Settings/SystemSetup/BatteryService\n/Settings/SystemSetup/HasAcOutSystem\n/Settings/SystemSetup/HasDcSystem\n/Settings/SystemSetup/MaxChargeCurrent\n/Settings/SystemSetup/MaxChargeVoltage\n/Settings/SystemSetup/SharedTemperatureSense\n/Settings/SystemSetup/SharedVoltageSense\n/Settings/SystemSetup/SystemName\n/Settings/SystemSetup/TemperatureService\n/Settings/Vrmlogger/HttpsEnabled\n/Settings/Vrmlogger/LogInterval\n/Settings/Vrmlogger/Logmode\n/Settings/Vrmlogger/RamDiskMode\n/Settings/Vrmlogger/Url\n/Settings/Watchdog/VrmTimeout\n/Settings/Devices/adc_builtin0_1/Standard2\n/Settings/Devices/adc_builtin0_1/Alarms/High/Active\n/Settings/Devices/adc_builtin0_1/Alarms/High/Delay\n/Settings/Devices/adc_builtin0_1/Alarms/High/Enable\n/Settings/Devices/adc_builtin0_1/Alarms/High/Restore\n/Settings/Devices/adc_builtin0_1/Alarms/Low/Active\n/Settings/Devices/adc_builtin0_1/Alarms/Low/Delay\n/Settings/Devices/adc_builtin0_1/Alarms/Low/Enable\n/Settings/Devices/adc_builtin0_1/Alarms/Low/Restore\n/Settings/Devices/adc_builtin0_1/Capacity\n/Settings/Devices/adc_builtin0_1/CustomName\n/Settings/Devices/adc_builtin0_1/FilterLength\n/Settings/Devices/adc_builtin0_1/FluidType2\n/Settings/Devices/adc_builtin0_1/Function\n/Settings/Devices/adc_builtin0_1/RawValueEmpty\n/Settings/Devices/adc_builtin0_1/RawValueFull\n/Settings/Devices/adc_builtin0_1/Shape\n/Settings/Devices/adc_builtin0_2/Standard2\n/Settings/Devices/adc_builtin0_2/Alarms/High/Active\n/Settings/Devices/adc_builtin0_2/Alarms/High/Delay\n/Settings/Devices/adc_builtin0_2/Alarms/High/Enable\n/Settings/Devices/adc_builtin0_2/Alarms/High/Restore\n/Settings/Devices/adc_builtin0_2/Alarms/Low/Active\n/Settings/Devices/adc_builtin0_2/Alarms/Low/Delay\n/Settings/Devices/adc_builtin0_2/Alarms/Low/Enable\n/Settings/Devices/adc_builtin0_2/Alarms/Low/Restore\n/Settings/Devices/adc_builtin0_2/Capacity\n/Settings/Devices/adc_builtin0_2/CustomName\n/Settings/Devices/adc_builtin0_2/FilterLength\n/Settings/Devices/adc_builtin0_2/FluidType2\n/Settings/Devices/adc_builtin0_2/Function\n/Settings/Devices/adc_builtin0_2/RawValueEmpty\n/Settings/Devices/adc_builtin0_2/RawValueFull\n/Settings/Devices/adc_builtin0_2/Shape\n/Settings/Devices/adc_builtin0_3/Standard2\n/Settings/Devices/adc_builtin0_3/Alarms/High/Active\n/Settings/Devices/adc_builtin0_3/Alarms/High/Delay\n/Settings/Devices/adc_builtin0_3/Alarms/High/Enable\n/Settings/Devices/adc_builtin0_3/Alarms/High/Restore\n/Settings/Devices/adc_builtin0_3/Alarms/Low/Active\n/Settings/Devices/adc_builtin0_3/Alarms/Low/Delay\n/Settings/Devices/adc_builtin0_3/Alarms/Low/Enable\n/Settings/Devices/adc_builtin0_3/Alarms/Low/Restore\n/Settings/Devices/adc_builtin0_3/Capacity\n/Settings/Devices/adc_builtin0_3/CustomName\n/Settings/Devices/adc_builtin0_3/FilterLength\n/Settings/Devices/adc_builtin0_3/FluidType2\n/Settings/Devices/adc_builtin0_3/Function\n/Settings/Devices/adc_builtin0_3/RawValueEmpty\n/Settings/Devices/adc_builtin0_3/RawValueFull\n/Settings/Devices/adc_builtin0_3/Shape\n/Settings/Devices/adc_builtin0_4/Standard2\n/Settings/Devices/adc_builtin0_4/Alarms/High/Active\n/Settings/Devices/adc_builtin0_4/Alarms/High/Delay\n/Settings/Devices/adc_builtin0_4/Alarms/High/Enable\n/Settings/Devices/adc_builtin0_4/Alarms/High/Restore\n/Settings/Devices/adc_builtin0_4/Alarms/Low/Active\n/Settings/Devices/adc_builtin0_4/Alarms/Low/Delay\n/Settings/Devices/adc_builtin0_4/Alarms/Low/Enable\n/Settings/Devices/adc_builtin0_4/Alarms/Low/Restore\n/Settings/Devices/adc_builtin0_4/Capacity\n/Settings/Devices/adc_builtin0_4/CustomName\n/Settings/Devices/adc_builtin0_4/FilterLength\n/Settings/Devices/adc_builtin0_4/FluidType2\n/Settings/Devices/adc_builtin0_4/Function\n/Settings/Devices/adc_builtin0_4/Shape\n/Settings/Devices/adc_builtin0_4/RawValueEmpty\n/Settings/Devices/adc_builtin0_4/RawValueFull\n/Settings/Devices/adc_builtin0_5/CustomName\n/Settings/Devices/adc_builtin0_5/FilterLength\n/Settings/Devices/adc_builtin0_5/Function\n/Settings/Devices/adc_builtin0_5/Offset\n/Settings/Devices/adc_builtin0_5/Scale\n/Settings/Devices/adc_builtin0_5/TemperatureType2\n/Settings/Devices/adc_builtin0_6/CustomName\n/Settings/Devices/adc_builtin0_6/FilterLength\n/Settings/Devices/adc_builtin0_6/Function\n/Settings/Devices/adc_builtin0_6/Offset\n/Settings/Devices/adc_builtin0_6/Scale\n/Settings/Devices/adc_builtin0_6/TemperatureType2\n/Settings/Devices/adc_builtin0_7/CustomName\n/Settings/Devices/adc_builtin0_7/FilterLength\n/Settings/Devices/adc_builtin0_7/Function\n/Settings/Devices/adc_builtin0_7/Offset\n/Settings/Devices/adc_builtin0_7/Scale\n/Settings/Devices/adc_builtin0_7/TemperatureType2\n/Settings/Devices/adc_builtin0_8/CustomName\n/Settings/Devices/adc_builtin0_8/FilterLength\n/Settings/Devices/adc_builtin0_8/Function\n/Settings/Devices/adc_builtin0_8/Offset\n/Settings/Devices/adc_builtin0_8/Scale\n/Settings/Devices/adc_builtin0_8/TemperatureType2\n/Settings/TempSensorRelay/adc_builtin0_5/0/ClearValue\n/Settings/TempSensorRelay/adc_builtin0_5/0/Relay\n/Settings/TempSensorRelay/adc_builtin0_5/0/SetValue\n/Settings/TempSensorRelay/adc_builtin0_5/1/ClearValue\n/Settings/TempSensorRelay/adc_builtin0_5/1/Relay\n/Settings/TempSensorRelay/adc_builtin0_5/1/SetValue\n/Settings/TempSensorRelay/adc_builtin0_5/Enabled\n/Settings/TempSensorRelay/adc_builtin0_6/0/ClearValue\n/Settings/TempSensorRelay/adc_builtin0_6/0/Relay\n/Settings/TempSensorRelay/adc_builtin0_6/0/SetValue\n/Settings/TempSensorRelay/adc_builtin0_6/1/ClearValue\n/Settings/TempSensorRelay/adc_builtin0_6/1/Relay\n/Settings/TempSensorRelay/adc_builtin0_6/1/SetValue\n/Settings/TempSensorRelay/adc_builtin0_6/Enabled\n/Settings/TempSensorRelay/adc_builtin0_7/0/ClearValue\n/Settings/TempSensorRelay/adc_builtin0_7/0/Relay\n/Settings/TempSensorRelay/adc_builtin0_7/0/SetValue\n/Settings/TempSensorRelay/adc_builtin0_7/1/ClearValue\n/Settings/TempSensorRelay/adc_builtin0_7/1/Relay\n/Settings/TempSensorRelay/adc_builtin0_7/1/SetValue\n/Settings/TempSensorRelay/adc_builtin0_7/Enabled\n/Settings/TempSensorRelay/adc_builtin0_8/0/ClearValue\n/Settings/TempSensorRelay/adc_builtin0_8/0/Relay\n/Settings/TempSensorRelay/adc_builtin0_8/0/SetValue\n/Settings/TempSensorRelay/adc_builtin0_8/1/ClearValue\n/Settings/TempSensorRelay/adc_builtin0_8/1/Relay\n/Settings/TempSensorRelay/adc_builtin0_8/1/SetValue\n/Settings/TempSensorRelay/adc_builtin0_8/Enabled\n"
  },
  {
    "path": "setup",
    "content": "#!/bin/bash\n\n# SetupHelper provides a set of utilities used by other packages to streamline installing and removing packages\n#\tand facilitates reinstallation following a Venus OS update\n# package setup scripts can be run from the command line for manual installation and uninstallation,\n#\tand in some cases inputing configuration options\n#\n# SetupHelper includes PackageManager which manages package updates from GitHub\n# \tas well as package installation an uninstallation from the main GUI\n#\n# this setup script does NOT use version-dependent file sets\n# \trather it makes modifications as part of this script\n#\tso that updates are not required when Venus OS versions are added\n\n# tell CommonResources to:\n#\tprompt for install/uninstall\n#\tauto install or auto uninstall\n#\tthen exit\n#\tCommonResources will NOT return here !\n\nstandardPromptAndActions='yes'\n\n#rebootNeeded=true\n#### following line incorporates helper resources into this script\nsource \"/data/SetupHelper/HelperResources/IncludeHelpers\"\n#### end of lines to include helper resources\n"
  },
  {
    "path": "updatePackage",
    "content": "#!/bin/bash\n\n# this script updates the package contents for all packages specified on the command line\n#\tor in the list below if 'all' is specified\n# file sets are updated/created for all Venus OS versions in the stockFiles directory\n#\n# NOTE: this script will create packages that will NOT work with SetupHelper prior to v6.0~2\n#\thowever helper resources are included in the package and are used instead of those provided by SH < v6.0~2\n#\tthe setup script for the package should source the local InstallHelpers file not CommonResources in SH\n#\n# if fileListVersionIndependent is present,\n#\tfiles are moved from the package directory to the VersionIndependent file set\n#\n# any .ALT_ORIG files are moved from the package directory to the AlternateOriginals directory\n#\n# replacement files may optionally be created from the original with a patch file\n#\tthe files to be patched are listed in fileListPatched\n#\tpatch files exist in FileSets/PatchSource\n#\tin order to create a patch file here, a source and edited file must also reside there\n#\n#\tpatched replacement files are created in CommonResources prior to returning control to the setup script\n#\n# This is a unix bash script and should be run on a host computer, not a GX device\n# Windows will not run this script natively.\n# However Windows 10 apparently supports bash:\n# https://www.howtogeek.com/249966/how-to-install-and-use-the-linux-bash-shell-on-windows-10/\n#\n# packages to be evaulated may be specified on the command line\n# use 'all' to process all packages in the allPackages list below\n#\n# file sets which contain real files (not just links and flags) \n#\tfor a version NOT contained in StockFiles will be flagged with UNUSED_FILE_SET\n# \tthis can occur if you remove versions from StockFiles.\n# \tFor example, you may wish to remove beta versions after a beta test cycle.\n# file sets which do not contain any real files\t(just symbolic links or flag files) are removed\n\n# stockFiles contains excerpts from Venus OS file systems\n# and must be stored on the host\n# within a directory with name of the exact Venus OS version\n# and within the stockFiles directory defined below.\n#\n# 1) missing file set directories are created\n# 2) if any files in fileList don't exist (eg, a new file was added to file list),\n#   the original file in stockFiles is copied to the version directory\n# 3) if the original file does not exist, the file is so marked with .NO_STOCK_FILE\n#\tthis situation must be corrected:\n# \t\tversion-dependent files without an original MUST use an \"alternate original\"\n#   \t\tspecified in FileSets. This permits version checks for these files too\n# \t\treplacement files that do not replace a stock file should be placed in version-indpendent file storage\n#\t \t\t(FileSets/).\n#\t\tIf these replacement files vary with Venus OS versions, they MUST include an \"alternate original\". \n#\t\tThis permits version checks for these files too\n#\n# when a stock file set does not exist, this script will check files from existing file sets\n#\tfor a matching original file.\n# \tIf a match is found, the replacement file is automatically placed in the new file set\n#\tIf no match is found, the missing replacement is flagged and a suitable replacement must be created manually.\n#\n# existing file sets not in the stockFiles are checked. If empty, they are removed.\n#\tIf not empty they are marked UNUSED_FILE_SET and flagged for manual removal.\n#\n# file sets will include all files listed in fileList.\n#\tthis allows the setup script to always have a replacement for known versions\n#\twithout searching other file sets for a matching original file\n#\t(there have been cases where installation fails because the search for a matching original could not be found)\n# if the stock file matches a previous version, a symbolic link for the replacement is created\n#\trather than duplicating the file\n# this also makes maintanence easier since matching replacement can be identified\n#\n# original files in the file set are not normally used when installing the package\n# however, they are retained so that the setup script can attempt to create a file set for an unknown Venus OS version\n# this of course may fail if a matching original file can not be found\n#\n# if no end action is specified on the command line, the user is prompted for how to proceed for each package processed\n# end actions specified will bypass this prompt and proceed with the next package\n# end actions:\n#\t-p\tdo not update the package but preserve the working copy\n#\t-d\tdo not update the package and deete the working copy\n#\t-u\tupdate the package with changes in the working copy's file sets\n#\n#\t-r restore package from backup if present - no processing is performed on the packge, backup or working copy\n#\t\tbackups are automatically created when updating a package\n#\t\tNote: the restore option is not offered at the end prompt since the update has not been applied yet.\n#\n# if errors occur, the needed corrections may be more obvious by comparing the package and the working copy\n#\tfor this reason, preserving the working copy is recommended if errors are expected\n\n# set allPackages to all packages this script should evalueate if no options are included\nallPackages=\"SetupHelper TailscaleGX ShutdownMonitor VeCanSetup RpiDisplaySetup RpiGpioSetup GuiMods\"\n## ExtTransferSwitch GeneratorConnector TankRepeater are obsolete and file sets should not be updated.\n\n\n# attempt to locate SharedUtilities based on the location of this script\n#\t(it is assumed to be in the SetupHelper directory)\n# also sets the package root directory based on this also\n# and also the stock files base directory\n#\n# if these are not correct, edit the lines below to set the appropriate values\n\nscriptDir=\"$( cd $(dirname \"$0\") >/dev/null 2>&1 ; /bin/pwd -P )\"\npackageRoot=\"$( dirname $scriptDir )\"\nstockFiles=\"$packageRoot/StockVenusOsFiles\"\n\n#### set these as appropriate to your system if the values set above are not correct\n#### packageRoot=FILL_THIS_IN_AND_UNCOMMENT_LINE\n#### stockFiles=FILL_THIS_IN_AND_UNCOMMENT_LINE\n\nif [ ! -e \"$packageRoot\" ]; then\n\techo \"unable to locate package root - can't continue\"\n\texit\nelif [ ! -e \"$stockFiles\" ]; then\n\techo \"unable to locate stock files - can't continue\"\n\texit\nfi\n# convert a version string to an integer to make comparisions easier\n#\n#\tNote: copied from VersionResources\n#\t\tbut also includes code to report duplcates not in the VersionResources version\n\nfunction versionStringToNumber ()\n{\n\tlocal version=\"$*\"\n\tlocal numberParts\n\tlocal versionParts\n\tlocal numberParts\n\tlocal otherParts\n\tlocal other\n\tlocal number=0\n\tlocal type='release'\n\n\t# split incoming string into\n\t# an array of numbers: major, minor, prerelease, etc\n\t# and an array of other substrings\n\t# the other array is searched for releasy type strings and the related offest added to the version number\n\t\n\tread -a numberParts <<< $(echo $version | tr -cs '0-9' ' ')\n\tnumberPartsLength=${#numberParts[@]}\n\tif (( $numberPartsLength == 0 )); then\n\t\tversionNumber=0\n\t\tversionStringToNumberStatus=\"$version: invalid, missing major version\"\n\t\treturn 1\n\tfi\n\tif (( $numberPartsLength >= 2 )); then\n\t\tread -a otherParts <<< $(echo $version | tr -s '0-9' ' ')\n\t\n\t\tfor other in ${otherParts[@]}; do\n\t\t\tcase $other in\n\t\t\t\t'b' | '~')\n\t\t\t\t\ttype='beta'\n\t\t\t\t\t(( number += 60000 ))\n\t\t\t\t\tbreak ;;\n\t\t\t\t'a')\n\t\t\t\t\ttype='alpha'\n\t\t\t\t\t(( number += 30000 ))\n\t\t\t\t\tbreak ;;\n\t\t\t\t'd')\n\t\t\t\t\ttype='develop'\n\t\t\t\t\tbreak ;;\n\t\t\tesac\n\t\tdone\n\tfi\n\n\t# if release all parts contribute to the main version number\n\t#\tand offset is greater than all prerelease versions\n\tif [ \"$type\" == \"release\" ] ; then\n\t\t(( number += 90000 ))\n\t# if pre-release, last part will be the pre release part\n\t#\tand others part will be part the main version number\n\telse\n\t\t(( numberPartsLength-- ))\n\t\t(( number += 10#${numberParts[$numberPartsLength]} ))\n\tfi\n\t# include core version number\n\t(( number += 10#${numberParts[0]} * 10000000000000 ))\n\tif (( numberPartsLength >= 2)); then\n\t\t(( number += 10#${numberParts[1]} * 1000000000 ))\n\tfi\n\tif (( numberPartsLength >= 3)); then\n\t\t(( number += 10#${numberParts[2]} * 100000 ))\n\tfi\n\n\tversionNumber=$number\n\tversionStringToNumberStatus=\"$version:$number $type\"\n\treturn 0\n}\n\n\n# getFileLists reads the file list from files in the FileSets directory\n#\n#\t'fileList' file must only list version-dependent files\n#\t'fileListVersionIndependent' file must list only version-independent files\n#\t\tprior to SetupHelper v6.0, this list is ignored\n#\n# $1 specifies where the path to the fileList files\n#\n# three composite file lists are returned in global arrays:\n#\tfileList contains only version-dependent files\n#\tfileListVersionIndependent contains only version-independent files\n#\tfileListAll contains both versioned and version-independent files\n#\n#\tNote: copied from CommonResources\n\nfunction getFileLists ()\n{\n\tlocal verListFile=\"$1/fileList\"\n\tlocal indListFile=\"$1/fileListVersionIndependent\"\n\tlocal patchListFile=\"$1/fileListPatched\"\n\tlocal tempListVer=()\n\tlocal tempListInd=()\n\tlocal tempListPatched=()\n\n\tif [ -f \"$verListFile\" ]; then\n\t\twhile read -r line || [[ -n \"$line\" ]]; do\n\t\t\tread -a params <<< $line\n\t\t\t# parse line into space-separted parameters then discard any that don't begin with /\n\t\t\t# this strips all comments beginning with # as well as any leading or trailing spaces\n\t\t\tfor param in ${params[@]} ; do\n\t\t\t\tcase $param in\n\t\t\t\t\t/*)\n\t\t\t\t\t\ttempListVer+=(\"$param\")\n\t\t\t\t\t\t;;\n\t\t\t\tesac\n\t\t\tdone\n\t\tdone < \"$verListFile\"\n\tfi\n\tif [ -f \"$indListFile\" ]; then\n\t\twhile read -r line || [[ -n \"$line\" ]]; do\n\t\t\tread -a params <<< $line\n\t\t\tfor param in ${params[@]} ; do\n\t\t\t\tcase $param in\n\t\t\t\t\t/*)\n\t\t\t\t\t\ttempListInd+=(\"$param\")\n\t\t\t\t\t\t;;\n\t\t\t\tesac\n\t\t\tdone\n\t\tdone < \"$indListFile\"\n\tfi\n\tif [ -f \"$patchListFile\" ]; then\n\t\twhile read -r line || [[ -n \"$line\" ]]; do\n\t\t\tread -a params <<< $line\n\t\t\tfor param in ${params[@]} ; do\n\t\t\t\tcase $param in\n\t\t\t\t\t/*)\n\t\t\t\t\t\ttempListPatched+=(\"$param\")\n\t\t\t\t\t\t;;\n\t\t\t\tesac\n\t\t\tdone\n\t\tdone < \"$patchListFile\"\n\tfi\n\n\t# remove duplicate files from each list\n\tfileList=($(printf \"%s\\n\" \"${tempListVer[@]}\" | sort -u))\n\tfileListVersionIndependent=($(printf \"%s\\n\" \"${tempListInd[@]}\" | sort -u))\n\tfileListPatched=($(printf \"%s\\n\" \"${tempListPatched[@]}\" | sort -u))\n\ttempListAll=(${fileList[@]})\n\ttempListAll+=(${fileListVersionIndependent[@]})\n\ttempListAll+=(${fileListPatched[@]})\n\tfileListAll=($(printf \"%s\\n\" \"${tempListAll[@]}\" | sort -u))\n\n\t# report duplicates\n\tlocal dupsVer=($(printf \"%s\\n\" \"${tempListVer[@]}\" | sort | uniq -d ))\n\tlocal dupsInd=($(printf \"%s\\n\" \"${tempListInd[@]}\" | sort | uniq -d ))\n\tlocal dupsPatched=($(printf \"%s\\n\" \"${tempListPatched[@]}\" | sort | uniq -d ))\n\tlocal dupsAll=($(printf \"%s\\n\" \"${tempListAll[@]}\" | sort | uniq -d ))\n\tif [ ! -z \"$dupsVer\" ]; then\n\t\tfor dup in \"$dupsVer\" ; do\n\t\t\tlogMessage \"WARNING $package: duplicate in fileList $dup - ignored\"\n\t\tdone\n\tfi\n\tif [ ! -z \"$dupsInd\" ]; then\n\t\tfor dup in \"$dupsInd\" ; do\n\t\t\tlogMessage \"WARNING $package: duplicate in fileListVersionIndependent $dup - ignored\"\n\t\tdone\n\tfi\n\tif [ ! -z \"$dupsPatched\" ]; then\n\t\tfor dup in \"$dupsPatched\" ; do\n\t\t\tlogMessage \"WARNING $package: duplicate in fileListPatched $dup - ignored\"\n\t\tdone\n\tfi\n\tif [ ! -z \"$dupsAll\" ]; then\n\t\tfor dup in \"$dupsAll\" ; do\n\t\t\tlogMessage \"ERROR $package: duplicate in combined file lists $dup - can't continue\"\n\t\tdone\n\t\texit\n\tfi\n}\n\n\ntotalErrors=0\ntotalWarnings=0\npackageErrors=0\npackageWarnings=0\n\noutputtingProgress=false\n\n\nfunction logMessage ()\n{\n\tif $outputtingProgress ; then\n\t\tclearProgress\n\tfi\n    echo \"$*\"\n    if [[ \"$*\" == \"ERROR\"* ]]; then\n        ((totalErrors++))\n        ((packageErrors++))\n    elif [[ \"$*\" == \"WARNING\"* ]]; then\n        ((totalWarnings++))\n        ((packageWarnings++))\n    fi\n}\n\nfunction outputProgressTick ()\n{\n\tif ! $outputtingProgress ; then\n\t\techo -en \"$beginProgressString\"\n\tfi\n\techo -en \"$1\"\n\toutputtingProgress=true\n}\n\nfunction clearProgress ()\n{\n\t# start a new line if outputting ticks\n\tif $outputtingProgress; then\n\t\techo\n\t\t# echo -ne \"\\r\\033[2K\" #### erase line\n\tfi\n\toutputtingProgress=false\n}\n\nbeginProgressString=\"\"\n\nfunction beginProgress ()\n{\n\t# erase the line but stay on it\n\tif $outputtingProgress ; then\n\t\tclearProgress\n\tfi\n\tif [ ! -z \"$1\" ]; then\n\t\tbeginProgressString=\"$1 \"\n\t\techo -en \"$beginProgressString\"\n\t\t\n\t\toutputtingProgress=true\n\tfi\n}\n\n# removing a nested set of directories sometimes results in permission denied the first time\n\t#\tso try several times to be sure\n\nfunction deleteNestedDirectories ()\n{\n\trm -rf \"$1\" &> /dev/null\n\tif [ -d \"$1\" ] ; then\n\t\trm -rf \"$1\" &> /dev/null\n\t\tif [ -d \"$1\" ] ; then\n\t\t\trm -rf \"$1\"\n\t\tfi\n\tfi\n}\n\n\nyesNoPrompt ()\n{\n    response=''\n    while true; do\n        /bin/echo -n \"$*\"\n        read response\n        case $response in\n            [yY]*)\n                return 0\n                break\n                ;;\n            [nN]*)\n                return 1\n                break\n                ;;\n            *)\n        esac\n    done\n}\n\n\n#### script code begins here\n\npackageList=\"\"\ndoAllPackages=false\nglobalEndAction=''\n\nfor param in $* ; do\n\tcase $param in\n\t\t-[pP]*)\n\t\t\tlogMessage \"working copies will be preserved - packages will not be updated\"\n\t\t\tglobalEndAction='preserve'\n\t\t\t;;\n\t\t-[dD]*)\n\t\t\tlogMessage \"working copies will be deleted - packages will not be updated\"\n\t\t\tglobalEndAction='delete'\n\t\t\t;;\n\t\t-[uU]*)\n\t\t\tlogMessage \"packages will be updated after updating\"\n\t\t\tglobalEndAction='update'\n\t\t\t;;\n\t\t-[rR]*)\n\t\t\tlogMessage \"packages will be restored from backups\"\n\t\t\tglobalEndAction='restore'\n\t\t\t;;\n\t\tall)\n\t\t\tdoAllPackages=true\n\t\t\t;;\n\t\t*)\n\t\t\tpackageList+=\" \"$1\n\tesac\n\tshift\ndone\nif $doAllPackages ; then\n    packageList=$allPackages\nelif [ -z \"$packageList\" ]; then\n\tlogMessage \"ERROR no packages specified - use 'all' for all packages\"\n\texit\nfi\n\nif [ \"$globalEndAction\" == \"restore\" ]; then\n\tfor package in $packageList; do\n\t\tsourceDirectory=\"$packageRoot/$package\"\n\t\tsourceFiles=\"$sourceDirectory/FileSets\"\n\t\tbackupDirectory=\"$packageRoot/$package.backup\"\n\t\tbackupFiles=\"$backupDirectory/FileSets\"\n\t\tsourceVeLib=\"$sourceDirectory/velib_python\"\n\t\tbackupVeLib=\"$backupDirectory/velib_python\"\n\t\tif [ ! -d \"$backupDirectory\" ]; then\n\t\t\tlogMessage \"WARNING $package: no backup found - package NOT restored\"\n\t\t\tcontinue\n\t\tfi\n\t\tlogMessage \"WARNING $package: restored from backup\"\n\t\tdeleteNestedDirectories \"$sourceFiles\"\n\t\tdeleteNestedDirectories \"$sourceVeLib\"\n\t\tmv \"$backupFiles\" \"$sourceFiles\"\n\t\tif [ -e \"$backupVeLib\" ]; then\n\t\t\tmv -f \"$backupVeLib\" \"$sourceVeLib\"\n\t\tfi\n\t\t#### TODO: delayed implementaiton \n\t\t####if [ -e \"$backupDirectory/validFirmwareVersions\" ]; then\n\t\t####\tmv -f \"$backupDirectory/validFirmwareVersions\" \"$sourceDirectory/validFirmwareVersions\"\n\t\t####fi\n\t\tdeleteNestedDirectories $backupDirectory\n\tdone\n\texit\nfi\n\n# get helper resources version for later\nif [ -f \"$packageRoot/SetupHelper/version\" ]; then\n\tshVersion=$( cat \"$packageRoot/SetupHelper/version\" )\n\tversionStringToNumber $shVersion\n\tshVersionNumber=$versionNumber\nelse\n\tshVersion=\"\"\n\tshVersionNumber=0\nfi\n\n\n# make the version list from the directories in stock files\n# version lists are sorted so the most recent version is first\ntempList=()\nstockVersionList=($(ls -d \"$stockFiles\"/v[0-9]* 2> /dev/null))\nfor entry in ${stockVersionList[@]} ; do\n    version=$(basename $entry)\n    versionFile=\"$stockFiles/$version/opt/victronenergy/version\"\n\tif [ -f \"$versionFile\" ]; then\n\t\trealVersion=$(cat \"$versionFile\" | head -n 1)\n\telse\n        logMessage \"ERROR version file missing from stock files $version - can't continue\"\n        exit\n\tfi\n\n    if [ $version != $realVersion ]; then\n        logMessage \"ERROR $version name does not mactch Venus $realVersion - can't continue\"\n        exit\n    fi\n\tif versionStringToNumber $version ; then\n\t\ttempList+=(\"$version:$versionNumber\")\n\telse\n\t\tlogMessage \"ERROR invalid version $versionStringToNumberStatus - not added to list\"\n\tfi\ndone\nstockVersionList=( $(echo ${tempList[@]} | tr ' ' '\\n' | sort -t ':' -r -n -k 2 | uniq ) )\nstockVersionListLength=${#stockVersionList[@]}\n\nfor package in $packageList; do\n\tpackageErrors=0\n\tpackageWarnings=0\n\n\tsourceDirectory=\"$packageRoot/$package\"\n\tsourceFiles=\"$sourceDirectory/FileSets\"\n\tworkingDirectory=\"$packageRoot/$package.copy\"\n\tworkingFiles=\"$workingDirectory/FileSets\"\n\tbackupDirectory=\"$packageRoot/$package.backup\"\n\tbackupFiles=\"$backupDirectory/FileSets\"\n\tversionIndependentFileSet=\"$workingFiles/VersionIndependent\"\n\tsourceVeLib=\"$sourceDirectory/velib_python\"\n\tworkingVeLib=\"$workingDirectory/velib_python\"\n\tbackupVeLib=\"$backupDirectory/velib_python\"\n\n    if [ ! -d \"$sourceDirectory\" ] || [ ! -f \"$sourceDirectory/version\" ]; then\n        logMessage \"WARNING: $sourceDirectory - not a package directory - skipping\"\n        continue\t# next package\n    fi\n    if [ ! -d \"$sourceFiles\" ]; then\n        logMessage \"$package: no file sets\"\n    fi\n    if ! [ -f \"$sourceFiles/fileList\" ]; then\n        logMessage \"$package: no version-dependent files\"\n    fi\n    if ! [ -f \"$sourceFiles/fileListVersionIndependent\" ]; then\n        logMessage \"$package: no version-independent files\"\n    fi\n    if ! [ -f \"$sourceFiles/fileListPatched\" ]; then\n        logMessage \"$package: no patches\"\n    fi\n\n\t# validate package version number\n\ttempVersion=$(cat \"$sourceDirectory/version\")\n\tif ! versionStringToNumber $tempVersion ; then\n\t\tlogMessage \"ERROR $package: version $versionStringToNumberStatus - skipping package\"\n\t\tcontinue\t# next package\n\tfi\n\n\t# compute compatible version range - use values in original package\n\tif [ -f \"$sourceDirectory/obsoleteVersion\" ]; then\n\t\tobsoleteVersio=$(cat \"$sourceDirectory/obsoleteVersion\")\n\t\tif versionStringToNumber $obsoleteVersio ; then\n\t\t\tobsoleteVersionNumber=$versionNumber\n\t\telse\n\t\t\tlogMessage \"ERROR $package obsoleteVersion $versionStringToNumberStatus - skipping package\"\n\t\t\tcontinue\t# next package\n\t\tfi\n\telse\n\t\tobsoleteVersionNumber=9999999999999999\n\tfi\n\tif [ -f \"$sourceDirectory/firstCompatibleVersion\" ]; then\n\t\tfirstCompatibleVersion=$(cat \"$sourceDirectory/firstCompatibleVersion\")\n\t# limit packages to v3.10 and newer\n\telse\n\t\tfirstCompatibleVersion='v3.10'\n\tfi\n\tif versionStringToNumber $firstCompatibleVersion ; then\n\t\tfirstVersionNumber=$versionNumber\n\telse\n\t\tlogMessage \"ERROR $package: firstCompatibleVersion $versionStringToNumberStatus - skipping package\"\n\t\tcontinue\t# next package\n\tfi\n\n\t# make copy of source package FileSets\n\treplaceCopy=false\n\tif [ -e \"$workingDirectory\" ]; then\n\t\tlogMessage \"$(basename $workingDirectory) already exists\"\n\t\tif yesNoPrompt \"  replace it (y) or continue updating the copy (n)? \" ; then\n\t\t\treplaceCopy=true\n\t\telse\n\t\t\tlogMessage \"$package: checking existing working copy\"\n\t\tfi\n\tfi\n\n\tif $replaceCopy || ! [ -e \"$workingDirectory\" ]; then\n\t\tif $replaceCopy ;then\n\t\t\tlogMessage \"$package: replacing working copy\"\n\t\telse\n\t\t\tlogMessage \"$package: making working copy\"\n\t\tfi\n\t\tdeleteNestedDirectories \"$workingDirectory\"\n\t\tmkdir -p \"$workingDirectory\"\n\t\tif [ -d \"$sourceFiles\" ]; then\n\t\t\tcp -pR \"$sourceFiles\" \"$workingFiles\"\n\t\tfi\n\t\tif [ -d \"$sourceVeLib\" ]; then\n\t\t\tcp -pR \"$sourceVeLib\" \"$workingVeLib\"\n\t\tfi\n\tfi\n\t# clean up flag files from a previous run\n\trm -f \"$workingFiles\"/*/INCOMPATIBLE_VERSION\n\trm -f \"$workingFiles\"/*/UNUSED_FILE_SET\n\trm -f \"$workingFiles\"/*/INCOMPLETE\n\trm -f \"$workingFiles\"/*/COMPLETE\n\trm -f \"$workingFiles\"/*/LINKS_ONLY\n\trm -f \"$workingFiles\"/*/*.NO_ORIG\n\trm -f \"$workingFiles\"/*/*.CHECK_REPLACEMENT\n\trm -f \"$workingFiles\"/*/*.CHECK_PATCH\n\trm -f \"$workingFiles\"/*/*.BAD_LINK\n\trm -f \"$workingFiles\"/*/NEW_FILE_SET\n\trm -f \"$workingFiles\"/*VERSIONED_AND_INDEPENDENT_EXIST\n\trm -f \"$workingFiles\"/*/*VERSIONED_AND_INDEPENDENT_EXIST\n\trm -f \"$workingFiles\"/*CHECK_VERSION_INDEPENDENT\n\trm -f \"$workingFiles\"/*/*CHECK_VERSION_INDEPENDENT\n\trm -f \"$workingFiles\"/*CHECK_ALT_ORIG\n\trm -f \"$workingFiles\"/*/*CHECK_ALT_ORIG\n\n\t#### TODO: delayed implementaiton # create valid firmware version list\n\t####rm -f \"$workingDirectory/validFirmwareVersions\"\n\t####for (( i1 = 0; i1 < $stockVersionListLength; i1++ )); do\n\t####\tIFS=':' read version versionNumber <<< \"${stockVersionList[$i1]}\"\n\t####\tif (( versionNumber >= firstCompatibleVersionNumber )) && (( versionNumber < obsoleteVersionNumber )) ; then\n\t####\t\techo $version >> \"$workingDirectory/validFirmwareVersions\"\n\t####\tfi\n\t####done\n\n\t# update velib_python\n\tif [ -e \"$workingVeLib\" ]; then\n\t\tbeginProgress \"updating velib_python\"\n\t\tpythonLibSoureDir=\"opt/victronenergy/dbus-systemcalc-py/ext/velib_python\"\n\t\tveLibFiles=( vedbus.py dbusmonitor.py settingsdevice.py ve_utils.py )\n\t\trm -rf \"$workingVeLib\"\n\t\tmkdir \"$workingVeLib\"\n\n\t\tfor (( i1 = 0; i1 < $stockVersionListLength; i1++ )); do\n\t\t\tnewVersion=false\n\t\t\tIFS=':' read version versionNumber <<< \"${stockVersionList[$i1]}\"\n\n\t\t\tif (( i1  == 0 )); then\n\t\t\t\tnewVersion=true\n\t\t\telse\n\t\t\t\tfor file in ${veLibFiles[@]} ; do\n\t\t\t\t\tfile1=\"$stockFiles/$version/$pythonLibSoureDir/$file\"\n\t\t\t\t\tfile2=\"$stockFiles/$previousVersion/$pythonLibSoureDir/$file\"\n\t\t\t\t\tif ! cmp -s \"$file1\" \"$file2\" > /dev/null ; then\n\t\t\t\t\t\tnewVersion=true\n\t\t\t\t\tfi\n\t\t\t\tdone\n\t\t\tfi\n\n\t\t\tif $newVersion ; then\n\t\t\t\toutputProgressTick \".\"\n\t\t\t\tif (( i1 == 0 ));then\n\t\t\t\t\tvelibDir=\"$workingVeLib/latest\"\n\t\t\t\t\tprevVelibDir=\"$workingVeLib/latest\"\n\t\t\t\telse\n\t\t\t\t\tvelibDir=\"$workingVeLib/$version\"\n\t\t\t\tfi\n\t\t\t\tmkdir \"$velibDir\"\n\t\t\t\tfor file in ${veLibFiles[@]} ; do\n\t\t\t\t\tfile1=\"$stockFiles/$version/$pythonLibSoureDir/$file\"\n\t\t\t\t\tfile2=\"$velibDir/$file\"\n\t\t\t\t\tcp -f \"$file1\" \"$file2\"\n\t\t\t\tdone\n\t\t\t\tnewVersion=false\n\t\t\t\tpreviousVersion=$version\n\t\t\t\tprevVelibDir=\"$velibDir\"\n\t\t\tfi\n\t\t\techo $version > \"$prevVelibDir/oldestVersion\"\n\t\tdone\n\tfi\n\n\tgetFileLists \"$workingFiles\"\n\n\t# if any version-dependent files, create missing file sets or flag incompatible\n\tif ! [ -z $fileList ]; then\n\t\tfor entry in ${stockVersionList[@]}; do\n\t\t\tIFS=':' read version versionNumber <<< \"$entry\"\n\t\t\tfileSet=\"$workingFiles/$version\"\n\t\t\tstockFileSet=\"$stockFiles/$version\"\n\n\t\t\tif (( $versionNumber >= $obsoleteVersionNumber )) || (( $versionNumber < $firstVersionNumber )); then\n\t\t\t\ttouch \"$fileSet/INCOMPATIBLE_VERSION\"\n\t\t\t\tcompatible=false\n\t\t\telse\n\t\t\t\tcompatible=true\n\t\t\tfi\n\n\t\t\tif $compatible && ! [ -e \"$fileSet\" ]; then\n\t\t\t\tmkdir \"$fileSet\"\n\t\t\t\ttouch \"$fileSet/NEW_FILE_SET\"\n\t\t\tfi\n\t\tdone\n\tfi\n\n\t# add the package's existing file sets NOT in the stock versions and mark them unused\n\t# replacement files will not be moved to these unused file sets\n\tsourceFileSets=($(ls -d \"$workingFiles\"/v[0-9]* 2> /dev/null))\n\ttempList=(${stockVersionList[@]})\n\tfor entry in ${sourceFileSets[@]} ; do\n\t\tversion=$(basename $entry)\n\t\tif [ ! -d \"$stockFiles/$version\" ]; then\n\t\t\tif ! versionStringToNumber $version ; then\n\t\t\t\tlogMessage \"ERROR $package: file set name $versionStringToNumberStatus - can't continue\"\n\t\t\t\texit\n\t\t\tfi\n\t\t\ttempList+=($version:$versionNumber)\n\t\t\ttouch \"$workingFiles/$version/UNUSED_FILE_SET\"\n\t\tfi\n\tdone\n\tallFileSets=( $(echo ${tempList[@]} | tr ' ' '\\n' | sort -t ':' -r -n -k 2 | uniq ) )\n\n\t# move incompatible versions to the end of the list\n\t#\tso that real files end up in a supported file set\n\tobsoleteFileSets=()\n\ttempList=()\n\tfor entry in ${allFileSets[@]} ; do\n\t\tIFS=':' read version versionNumber <<< \"$entry\"\n\t\tif (( $versionNumber >= $obsoleteVersionNumber )) || (( $versionNumber < $firstVersionNumber )); then\n\t\t\tobsoleteFileSets+=($version:$versionNumber)\n\t\telse\n\t\t\ttempList+=($version:$versionNumber)\n\t\tfi\n\tdone\n\tallFileSets=(${tempList[@]})\n\tallFileSets+=(${obsoleteFileSets[@]})\n\tallFileSetsLength=${#allFileSets[@]}\n\n\t# relocate version-independent files to VersionIndependent file set\n\tfor file in ${fileListVersionIndependent[@]}; do\n\t\tbaseName=$(basename $file)\n\t\tif [ -f \"$workingFiles/$baseName\" ]; then\n\t\t\tif [ ! -d \"$workingFiles/VersionIndependent\" ]; then\n\t\t\t\tmkdir -p \"$workingFiles/VersionIndependent\"\n\t\t\tfi\n\t\t\tif [ -f \"$workingFiles/VersionIndependent/$baseName\" ]; then\n\t\t\t\tlogMessage \"$package: $baseName exists in FileSets AND VersionIndependent - not moved\"\n\t\t\t\ttouch \"$workingFiles/VersionIndependent/$baseName.CHECK_VERSION_INDEPENDENT\"\n\t\t\t\ttouch \"$workingFiles/$baseName.CHECK_VERSION_INDEPENDENT\"\n\t\t\telse\n\t\t\t\tlogMessage \"$package: moving $baseName to version-independent file set\"\n\t\t\t\tmv \"$workingFiles/$baseName\" \"$workingFiles/VersionIndependent\"\n\t\t\tfi\n\t\telif ! [ -f \"$workingFiles/VersionIndependent/$baseName\" ]; then\n\t\t\tlogMessage \"ERROR $package: $baseName missing version independent file\"\n\t\tfi\n\tdone\n\n\t# relocate ALT_ORIG files to .../FileSets/AlternateOriginals\n\t# do as a loop so each move is reported\n\t# must have some alt orig files in FileSets for this to happen\n\taltOrigFileDir=\"$workingFiles/AlternateOriginals\"\n\toldAltOrigList=( $( ls \"$workgingFiles\"/*.ALT_ORIG 2> /dev/null) )\n\tif ! [ -z \"$oldAltOrigList\" ] && yesNoPrompt \"move alternate originals to AlternateOriginal directory (y/n)? \" ; then\n\t\tif ! [ -d \"$altOrigFileDir\" ]; then\n\t\t\tmkdir -p \"$altOrigFileDir\"\n\t\tfi\n\t\tfor file in ${altOrigList[@]} ; do\n\t\t\tbaseName=$(basename \"$file\")\n\t\t\tif [ -f \"$altOrigFileDir/$baseName\" ]; then\n\t\t\t\tlogMessage \"$package: $baseName exists in FileSets AND AlternateOriginals - not moved\"\n\t\t\t\ttouch \"$altOrigFileDir/$baseName.CHECK_ALT_ORIG\"\n\t\t\t\ttouch \"$workingFiles/$baseName.CHECK_ALT_ORIG\"\n\t\t\telse\n\t\t\t\tlogMessage \"$package: moving $baseName.ALT_ORIG to AlternateOriginals\"\n\t\t\t\tmv \"$file\" \"$altOrigFileDir\"\n\t\t\tfi\n\t\tdone\n\tfi\n\n\t# create and test patch files\n\t# the option to skip creation exists in case the patch file needs to be hand edited\n\t#\n\t# in order to create a patch file, both an unmodified file without the changes\n\t#\tand an 'result' file WITH the desired changes are required\n\t#\n\t# multiple sets can exist and are of the form\n\t#\tresult\t$baseName*\t\t\teg PageSettings.qml-1\n\t#\torig\t$baseName*.orig\t\teg PageSettings.qml-1.orig\n\t#\tpatch\t$baseName*.patch\teg PageSettings.qml-1.patch\n\t#\n\t# legacy .edited and .source, will be converted to the new format\n\tpatchSourceDir=\"$workingFiles/PatchSource\"\n\tif (( ${#fileListPatched[@]} > 0 )); then\n\t\tbeginProgress \"checking patch files\"\n\t\tfor file in ${fileListPatched[@]} ; do\n\t\t\toutputProgressTick \".\"\n\t\t\tbaseName=$( basename $file )\n\t\t\tpatchOptionsFile=\"$patchSourceDir/$baseName.patchOptions\"\n\t\t\tif [ -f \"$patchOptionsFile\" ]; then\n\t\t\t\tpatchOptions=$( cat \"$patchOptionsFile\" )\n\t\t\t\toptionsModTime=$( date -r \"$patchOptionsFile\" '+%s' )\n\t\t\telse\n\t\t\t\tpatchOptions='-u'\n\t\t\t\toptionsModTime=0\n\t\t\tfi\n\t\t\t# convert old name formats\n\t\t\toldPath=\"$patchSourceDir/$baseName\"\n\t\t\tnewPath=\"$oldPath-1\"\n\t\t\tif [ -e \"$oldPath.source\" ] || [ -e \"$oldPath.edited\" ]; then\n\t\t\t\tif [ -e \"$newPath*\" ]; then\n\t\t\t\t\tlogMessage \"WARNING $package: can't move .source and .edited patch files - ...-1... already exists\"\n\t\t\t\telse\n\t\t\t\t\tif [ -e \"$oldPath.source\" ]; then\n\t\t\t\t\t\tlogMessage \"$package: renaming $baseName.source to $baseName.orig\"\n\t\t\t\t\t\tmv \"$oldPath.source\" \"$newPath.orig\"\n\t\t\t\t\tfi\n\t\t\t\t\tif [ -e \"$oldPath.edited\" ]; then\n\t\t\t\t\t\tlogMessage \"$package: renaming $baseName.edited to $baseName\"\n\t\t\t\t\t\tmv \"$oldPath.edited\" \"$newPath\"\n\t\t\t\t\tfi\n\t\t\t\tfi\n\t\t\tfi\n\n\t\t\tif [ \"$patchOptions\" == \"MANUAL\" ]; then\n\t\t\t\tlogMessage \"WARNING $package: $baseName patch options set to MANUAL - skipping patch update\"\n\t\t\telse\n\t\t\t\tpatchOrigFiles=( $( ls \"$patchSourceDir/$baseName\"*.orig 2> /dev/null ) )\n\t\t\t\tfor patchOrig in ${patchOrigFiles[@]}; do\n\t\t\t\t\tpatchResult=\"${patchOrig/.orig/}\"\n\t\t\t\t\tpatchFile=\"${patchOrig/.orig/.patch}\"\n\n\t\t\t\t\tif ! [ -f \"$patchResult\" ]; then\n\t\t\t\t\t\tlogMessage \"WARNING $package: missing $( basename $patchResult) - skipping patch update\"\n\t\t\t\t\t\tcontinue\n\t\t\t\t\tfi\n\n\t\t\t\t\tdiff $patchOptions \"$patchOrig\" \"$patchResult\" > \"$patchFile\"\n\t\t\t\tdone\n\t\t\tfi\n\n\t\t\t# test each patch on all versions - need one to succeed for each version\n\t\t\t#\tboth foward and reverse patches must succeed\n\t\t\t# \tand reverse patch result must match stock file\n\t\t\tpatchFiles=( $( ls \"$patchSourceDir/$baseName\"*.patch 2> /dev/null ) )\n\t\t\tfor (( i1 = 0; i1 < $allFileSetsLength; i1++ )); do\n\t\t\t\tIFS=':' read version versionNumber <<< \"${allFileSets[$i1]}\"\n\t\t\t\tstockFile=\"$stockFiles/$version$file\"\n\t\t\t\treplacement=\"$workingFiles/$version/$baseName\"\n\t\t\t\tif ! [ -f \"$stockFile\" ]; then\n\t\t\t\t\tcontinue\n\t\t\t\t# check for case mismatch (main vs Main is NOT a match)\n\t\t\t\telse\n\t\t\t\t\tresult=$( find $( dirname \"$stockFile\" ) -name $( basename \"$stockFile\" ) )\n\t\t\t\t\tif [ \"$result\" == \"\" ]; then\n\t\t\t\t\t\tcontinue\n\t\t\t\t\tfi\n\t\t\t\tfi\n\t\t\t\tfoundPatch=false\n\t\t\t\tfor patchFile in ${patchFiles[@]}; do\n\t\t\t\t\tpatchResultFile=\"$patchSourceDir/$baseName.patchResult\"\n\t\t\t\t\treversePatchResultFile=\"$patchSourceDir/$baseName.reversePatchResult\"\n\t\t\t\t\tpatchOk=true\n\n\t\t\t\t\tif ! yes 'no' | patch -N -o \"$patchResultFile\" \"$stockFile\" \"$patchFile\" > /dev/null ; then\n\t\t\t\t\t\tpatchOk=false\n\t\t\t\t\tfi\n\t\t\t\t\tif $patchOK && ! yes 'no' | patch -R -o \"$reversePatchResultFile\" \"$patchResultFile\" \"$patchFile\" > /dev/null ; then\n\t\t\t\t\t\tpatchOk=false\n\t\t\t\t\tfi\n\t\t\t\t\tif $patchOK && ! cmp -s \"$stockFile\" \"$reversePatchResultFile\" > /dev/null ; then\n\t\t\t\t\t\tpatchOk=false\n\t\t\t\t\tfi\n\t\t\t\t\trm -f \"$patchResultFile\"* \"$reversePatchResultFile\"*\n\t\t\t\t\t# found a patch that works - stop looking\n\t\t\t\t\tif $patchOk ; then\n\t\t\t\t\t\tfoundPatch=true\n\t\t\t\t\t\tbreak\n\t\t\t\t\tfi\n\t\t\t\tdone\n\t\t\t\t# if patch succeeded, remove any replacement and orig files in this file set\n\t\t\t\t#\tthey won't be looked at below since fileList won't include them\n\t\t\t\t#\tchecks above insure there the same file is not in multiple file lists\n\t\t\t\tif $foundPatch; then\n\t\t\t\t\tif [ -e \"$replacement\" ] && ! [ -L \"$replacement\" ] || [ -e \"$replacement.USE_ORIGINAL\" ]; then\n\t\t\t\t\t\tlogMessage \"WARNING $package: removing $baseName from $version file set\"\n\t\t\t\t\t\trm -f \"$replacement\"*\n\t\t\t\t\tfi\n\t\t\t\telse\n\t\t\t\t\tlogMessage \"ERROR $package: $version no patch file for $baseName\"\n\t\t\t\t\torigFile=\"$patchSourceDir/$baseName-$version\".orig\n\t\t\t\t\tif ! [ -e \"$origFile\" ]; then\n\t\t\t\t\t\tlogMessage \"  adding $baseName-$version.orig - replacement must be created manually\"\n\t\t\t\t\t\tcp \"$stockFile\" \"$origFile\"\n\t\t\t\t\tfi\n\t\t\t\t\ttouch \"$patchSourceDir/$baseName-$version\".CHECK_REPLACEMENT\n\t\t\t\t\ttouch \"$patchSourceDir/$baseName-$version\".CHECK_PATCH\n\t\t\t\t\ttouch \"$patchSourceDir/INCOMPLETE\"\n\t\t\t\tfi\n\t\t\tdone\n\t\tdone # for file in fileListPatched\n\tfi\t# check patch files\n\tbeginProgress \"$package: updating file sets\"\n\n\t# process only versioned files\n\tfor file in ${fileList[@]} ; do\n\t\toutputProgressTick \".\"\n\t\tbaseName=$(basename \"$file\")\n\t\tversionedFileExists=false\n\n\t\t# use alternate original if present in AlternateOriginals\n\t\tif [ -f \"$altOrigFileDir/$baseName.ALT_ORIG\" ]; then\n\t\t\tuseAltOrig=true\n\t\t\taltOrigFile=$(cat \"$altOrigFileDir/$baseName.ALT_ORIG\")\n\t\t# or in FileSets\n\t\telif [ -f \"$workingFiles/$baseName.ALT_ORIG\" ]; then\n\t\t\tuseAltOrig=true\n\t\t\taltOrigFile=$(cat \"$workingFiles/$baseName.ALT_ORIG\")\n\t\telse\n\t\t\tuseAltOrig=false\n\t\t\taltOrigFile=\"\"\n\t\tfi\n\n\t\t# locate groups of file sets with matching stock (or .orig) files\n\t\t(( start = 0 ))\n\t\twhile (( start < $allFileSetsLength )); do\n\t\t\t# locate one group of file sets that will use the same replacement file\n\t\t\t(( end = start )); (( to = -1 ))\n\t\t\ttoFileSet=\"\"\n\t\t\tcompareReference=\"\"\n\t\t\tstartVersion=\"-\"\n\t\t\ttoVersion=\"-\"\n\t\t\tendVersion=\"-\"\n\t\t\toldReplacementVersion=\"-\"\n\t\t\toldReplacementIsFile=false\n\t\t\toldReplacementIsUseOrig=false\n\t\t\taddOrigToFileSet=false\n\t\t\tblockIsUsed=false\n\t\t\tfor (( i1 = start; i1 < $allFileSetsLength; i1++ )); do\n\t\t\t\tIFS=':' read version versionNumber <<< \"${allFileSets[$i1]}\"\n\t\t\t\tfileSet=\"$workingFiles/$version\"\n\t\t\t\tif [ -e \"$fileSet/INCOMPATIBLE_VERSION\" ] || [ -e \"$fileSet/UNUSED_FILE_SET\" ]; then\n\t\t\t\t\tfileSetUsed=false\n\t\t\t\telse\n\t\t\t\t\tfileSetUsed=true\n\t\t\t\tfi\n\t\t\t\treplacement=\"$fileSet/$baseName\"\n\t\t\t\tstockFileSet=\"$stockFiles/$version\"\n\t\t\t\torig=\"$fileSet/$baseName.orig\"\n\t\t\t\tif $useAltOrig ; then\n\t\t\t\t\tstockFile=\"$stockFileSet$altOrigFile\"\n\t\t\t\telse\n\t\t\t\t\tstockFile=\"$stockFileSet$file\"\n\t\t\t\tfi\n\t\t\t\treplacementIsLink=false\n\t\t\t\treplacementIsFile=false\n\t\t\t\tuseOrig=false\n\t\t\t\tif [ -L \"$replacement\" ]; then\n\t\t\t\t\treplacementIsLink=true\n\t\t\t\telif [ -f \"$replacement\" ]; then\n\t\t\t\t\treplacementIsFile=true\n\t\t\t\telif [ -f \"$replacement.USE_ORIGINAL\" ]; then\n\t\t\t\t\tuseOrig=true\n\t\t\t\tfi\n\t\t\t\t# skip this version if parent directory does not exist\n\t\t\t\tif ! [ -e $( dirname \"$stockFile\" ) ]; then\n\t\t\t\t\tfileSetUsed=false\n\t\t\t\tfi\n\t\t\t\tif (( i1 == start )); then\n\t\t\t\t\tstartVersion=$version\n\t\t\t\tfi\n\t\t\t\n\t\t\t\tif ! $fileSetUsed ; then\n\t\t\t\t\tthisOrig=\"\"\n\t\t\t\telif [ -e \"$stockFile\" ]; then\n\t\t\t\t\tthisOrig=\"$stockFile\"\n\t\t\t\t# error if file set exists but no stock file\n\t\t\t\telif [ -e \"$stockFileSet\" ]; then\n\t\t\t\t\tif $useAltOrig ; then\n\t\t\t\t\t\tlogMessage \"ERROR $package: $version $baseName stock file missing - check ALT_ORIG - can't continue\"\n\t\t\t\t\t\ttouch \"$fileSet/$baseName.CHECK_ALT_ORIG\"\n\t\t\t\t\telse\n\t\t\t\t\t\tlogMessage \"ERROR $package: $version $baseName stock file missing - consider using an ALT_ORIG - can't continue\"\n\t\t\t\t\tfi\n\t\t\t\t\ttouch \"$fileSet/$baseName.NO_STOCK_FILE\"\n\t\t\t\t\ttouch \"$fileSet/INCOMPLETE\"\n\t\t\t\t\texit\n\t\t\t\t# no stock file - use orig in file set\n\t\t\t\telif [ -e \"$orig\" ]; then\n\t\t\t\t\tthisOrig=\"$orig\"\n\t\t\t\telse\n\t\t\t\t\tthisOrig=\"\"\n\t\t\t\tfi\n\t\t\t\tif [ -z \"$compareReference\" ] && ! [ -z \"$thisOrig\" ]; then\n\t\t\t\t\tcompareReference=\"$thisOrig\"\n\t\t\t\tfi\n\t\t\t\tincludeInBlock=false\n\t\t\t\t# nothing to compare - include in same block\n\t\t\t\tif $fileSetUsed ; then\n\t\t\t\t\tif [ -z \"$compareReference\" ] || [ -z \"$thisOrig\" ]; then\n\t\t\t\t\t\tincludeInBlock=true\n\t\t\t\t\telif [ \"$thisOrig\" == \"$compareReference\" ]; then\n\t\t\t\t\t\tincludeInBlock=true\n\t\t\t\t\t# orig exists and DOES match others in block\n\t\t\t\t\telif cmp -s \"$thisOrig\" \"$compareReference\" > /dev/null ; then\n\t\t\t\t\t\tincludeInBlock=true\n\t\t\t\t\tfi\n\t\t\t\tfi\n\n\t\t\t\t# start a new block\n\t\t\t\tif ! $includeInBlock ; then\n\t\t\t\t\tbreak\n\t\t\t\tfi\n\n\t\t\t\tif $fileSetUsed ; then\n\t\t\t\t\tblockIsUsed=true\n\t\t\t\tfi\n\n\t\t\t\t# save version of old replacement files for next loop\n\t\t\t\tif $replacementIsFile ; then\n\t\t\t\t\tif ! $oldReplacementIsFile || [ \"$oldReplacementVersion\" == \"-\" ]; then\n\t\t\t\t\t\toldReplacementVersion=$version\n\t\t\t\t\t\toldReplacementIsFile=true\n\t\t\t\t\t\toldReplacementIsUseOrig=false\n\t\t\t\t\t\taddOrigToFileSet=true\n\t\t\t\t\tfi\n\t\t\t\telif $useOrig && [ \"$oldReplacementVersion\" == \"-\" ]; then\n\t\t\t\t\toldReplacementVersion=$version\n\t\t\t\t\toldReplacementIsFile=false\n\t\t\t\t\toldReplacementIsUseOrig=true\n\t\t\t\t\t# retain .orig so that SetupHelper can create USE_ORIGINAL flag files for missing file sets\n\t\t\t\t\taddOrigToFileSet=true\n\t\t\t\tfi\n\n\t\t\t\t(( end = i1 ))\n\t\t\t\tendVersion=$version\n\n\t\t\t\t# first compatible file set - move replacements here\n\t\t\t\t# unused file sets are permitted for the destination file set\n\t\t\t\t# but if a used file set is later found, it is preferred\n\t\t\t\tif (( to == -1 )) && $fileSetUsed; then\n\t\t\t\t\t(( to = i1 ))\n\t\t\t\t\ttoFileSet=\"$fileSet\"\n\t\t\t\t\ttoVersion=$( basename \"$toFileSet\" )\n\t\t\t\tfi\n\t\t\tdone\t# end locate block\n\t\t\tif (( to == -1 )); then\n\t\t\t\tif $blockIsUsed; then\n\t\t\t\t\tlogMessage \"ERROR $package: $baseName no destination file set for block $startVersion $endVersion - can't relocate files\"\n\t\t\t\tfi\n\t\t\telse\n\t\t\t\ttoReplacement=\"$toFileSet/$baseName\"\n\t\t\t\toldReplacement=\"$workingFiles/$oldReplacementVersion/$baseName\"\n\t\t\t\ttoStockFileSet=\"$stockFiles/$toVersion\"\n\t\t\t\tif $useAltOrig ; then\n\t\t\t\t\ttoStockFile=\"$toStockFileSet$altOrigFile\"\n\t\t\t\telse\n\t\t\t\t\ttoStockFile=\"$toStockFileSet$file\"\n\t\t\t\tfi\n\n\t\t\t\t# relocate replacement & orig\n\t\t\t\tif $oldReplacementIsUseOrig ; then\n\t\t\t\t\trm -f \"$toReplacement\"*\n\t\t\t\t\ttouch \"$toReplacement.USE_ORIGINAL\"\n\t\t\t\t\tversionedFileExists=true\n\t\t\t\telif $oldReplacementIsFile; then\n\t\t\t\t\tif [ \"$oldReplacementVersion\" != \"$toVersion\" ]; then\n\t\t\t\t\t\trm -f \"$toReplacement\"*\n\t\t\t\t\t\tmv \"$oldReplacement\" \"$toReplacement\"\n\t\t\t\t\tfi\n\t\t\t\t\tversionedFileExists=true\n\t\t\t\telif [ -e $( dirname \"$toReplacement\" ) ]; then\n\t\t\t\t\ttouch \"$toReplacement.CHECK_REPLACEMENT\"\n\t\t\t\tfi\n\t\t\t\tif $addOrigToFileSet ; then\n\t\t\t\t\tif [ -f \"$toStockFile\" ]; then\n\t\t\t\t\t\tcp \"$toStockFile\" \"$toReplacement.orig\"\n\t\t\t\t\telif [ -f \"$oldReplacement.orig\" ]; then\n\t\t\t\t\t\tmv \"$oldReplacement.orig\" \"$toReplacement.orig\"\n\t\t\t\t\telse\n\t\t\t\t\t\tlogMessage \"ERROR $package: $baseName no original for $toVersion\"\n\t\t\t\t\t\ttouch \"$toReplacement.NO_ORIG\"\n\t\t\t\t\tfi\n\t\t\t\tfi\n\n\t\t\t\t# update links and look for additional replacements\n\t\t\t\tfor (( i1 = start; i1 <= end; i1++ )); do\n\t\t\t\t\t# skip to -- it's alredy updated\n\t\t\t\t\tif (( i1 == to )); then continue; fi\n\n\t\t\t\t\tIFS=':' read version versionNumber <<< \"${allFileSets[$i1]}\"\n\t\t\t\t\tfileSet=\"$workingFiles/$version\"\n\t\t\t\t\treplacement=\"$fileSet/$baseName\"\n\t\t\t\t\treplacementIsLink=false\n\t\t\t\t\treplacementIsFile=false\n\t\t\t\t\tuseOrig=false\n\t\t\t\t\tif [ -L \"$replacement\" ]; then\n\t\t\t\t\t\treplacementIsLink=true\n\t\t\t\t\telif [ -f \"$replacement\" ]; then\n\t\t\t\t\t\treplacementIsFile=true\n\t\t\t\t\telif [ -f \"$replacement.USE_ORIGINAL\" ]; then\n\t\t\t\t\t\tuseOrig=true\n\t\t\t\t\tfi\n\n\t\t\t\t\tif $useOrig ; then\n\t\t\t\t\t\tif ! $oldReplacementIsUseOrig ; then\n\t\t\t\t\t\t\tlogMessage \"WARNING $package: $baseName has replacement - USE_ORIGINAL removed\"\n\t\t\t\t\t\t\trm -f \"$replacement.USE_ORIGINAL\"\n\t\t\t\t\t\tfi\n\t\t\t\t\tfi\n\n\t\t\t\t\t# a second replacement is found - remove if matches, error if different\n\t\t\t\t\tupdateReplacements=true\n\t\t\t\t\tif $replacementIsFile && $oldReplacementIsFile; then\n\t\t\t\t\t\tif ! cmp -s \"$replacement\" \"$toReplacement\" > /dev/null ; then\n\t\t\t\t\t\t\tlogMessage \"ERROR $package: $baseName $version second replacement differs from $toVersion - check replacements\"\n\t\t\t\t\t\t\ttouch \"$replacement.CHECK_REPLACEMENT\"\n\t\t\t\t\t\t\ttouch \"$toReplacement.CHECK_REPLACEMENT\"\n\t\t\t\t\t\t\tupdateReplacements=false\n\t\t\t\t\t\tfi\n\t\t\t\t\tfi\n\t\t\t\t\tif $updateReplacements ; then\n\t\t\t\t\t\trm -f \"$replacement\"*\n\t\t\t\t\t\tif $oldReplacementIsUseOrig; then\n\t\t\t\t\t\t\ttouch \"$replacement.USE_ORIGINAL\"\n\t\t\t\t\t\t# links are created even if replacement does not exist\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\tln -sf \"../$toVersion/$baseName\" \"$replacement\"\n\t\t\t\t\t\tfi\n\t\t\t\t\tfi\n\t\t\t\tdone\n\t\t\tfi\n\t\t\t# continue looking for blocks with file set after the end of this block\n\t\t\t(( start = end + 1 ))\n\t\tdone # while start ...\n\n\t# both versioned and version-independent files exist - report this (no action taken)\n\tif $versionedFileExists; then\n\t\tif [ -e \"$versionIndependentFileSet/$baseName\" ];then\n\t\t\tlogMessage \"WARNING $package: $baseName versioned file exists - version-independent file will be ignored\"\n\t\t\ttouch \"$versionIndependentFileSet/$baseName.VERSIONED_AND_INDEPENDENT_EXIST\"\n\t\tfi\n\tfi\n\n    done  # for file\n\n\tbeginProgress \"$package: final checks\"\n\tfor (( i1 = 0; i1 < $allFileSetsLength; i1++ )); do\n\t\tIFS=':' read version versionNumber <<< \"${allFileSets[$i1]}\"\n\n\t\tif (( $versionNumber >= $obsoleteVersionNumber )) || (( $versionNumber < $firstVersionNumber )); then\n\t\t\tincompatibleVersion=true\n\t\telse\n\t\t\tincompatibleVersion=false\n\t\tfi\n\n\t\tfileSet=\"$workingFiles/$version\"\n\t\t# file set should exist so this shouldn't happen but report and skip checks anyway\n\t\tif ! [ -d \"$fileSet\" ]; then\n\t\t\tif ! $incompatibleVersion && ! [ -z $fileList ]; then\n\t\t\t\tlogMessage \"ERROR $package: $version missing file set - skipping\"\n\t\t\tfi\n\t\t\tcontinue\n\t\tfi\n\t\toutputProgressTick \".\"\n\n\t\treplacementFilesExist=false\n\t\tfor file in ${fileList[@]} ; do\n\t\t\tbaseName=$(basename \"$file\")\n\t\t\treplacement=\"$fileSet/$baseName\"\n\t\t\tif [ -f \"$replacement\" ] && ! [ -L \"$replacement\" ]; then\n\t\t\t\treplacementFilesExist=true\n\t\t\t\tbreak\n\t\t\tfi\n\t\tdone\n\t\t# LINKS_ONLY is not used for anything but helps identify file sets that don't contain real files\n\t\tif ! $replacementFilesExist ; then\n\t\t\ttouch \"$fileSet/LINKS_ONLY\"\n\t\tfi\n\n\t\tfileSetInUse=true\n\t\t# remove file sets for incompatible Venus OS versions\n\t\tif [ -e \"$fileSet/INCOMPATIBLE_VERSION\" ]; then\n\t\t\tfileSetInUse=false\n\t\t\tif $replacementFilesExist ; then\n\t\t\t\tlogMessage \"WARNING $package: $version not compatible with Venus $version but file set not empty - not removed\"\n\t\t\telse\n\t\t\t\tif [ ! -f \"$fileSet/NEW_FILE_SET\" ]; then\n\t\t\t\t\tlogMessage \"WARNING $package: not compatible with Venus $version - file set removed\"\n\t\t\t\tfi\n\t\t\t\trm -Rf \"$fileSet\"\n\t\t\tfi\n\t\t# remove empty unused file sets\n\t\telif [ -f \"$fileSet/UNUSED_FILE_SET\" ]; then\n\t\t\tfileSetInUse=false\n\t\t\tif $replacementFilesExist ; then\n\t\t\t\tlogMessage \"WARNING $package: $version no longer used but file set not empty - not removed\"\n\t\t\telse\n\t\t\t\t# log removal of a previous file set if not created with this run\n\t\t\t\t# if it was created with this run, delete it silently\n\t\t\t\tif [ ! -f \"$fileSet/NEW_FILE_SET\" ]; then\n\t\t\t\t\tlogMessage \"WARNING $package: $version - removing unused file set\"\n\t\t\t\tfi\n\t\t\t\trm -Rf \"$fileSet\"\n\t\t\tfi\n\t\tfi\n\t\t# do final checks on versioned files only\n\t\t#\tand for file sets that still exist\n\t\tif [ -e \"$fileSet\" ] && $fileSetInUse; then\n\t\t\tif [ -e \"$fileSet/NEW_FILE_SET\" ]; then\n\t\t\t\tlogMessage \"$package: $version new file set\"\n\t\t\tfi\n\t\t\tfor file in ${fileList[@]} ; do\n\t\t\t\tbaseName=$(basename \"$file\")\n\t\t\t\t# use alternate original if present in AlternateOriginals\n\t\t\t\tif [ -f \"$altOrigFileDir/$baseName.ALT_ORIG\" ]; then\n\t\t\t\t\tuseAltOrig=true\n\t\t\t\t\taltOrigFile=$(cat \"$altOrigFileDir/$baseName.ALT_ORIG\")\n\t\t\t\t# or if in FileSets\n\t\t\t\telif [ -f \"$workingFiles/$baseName.ALT_ORIG\" ]; then\n\t\t\t\t\tuseAltOrig=true\n\t\t\t\t\taltOrigFile=$(cat \"$workingFiles/$baseName.ALT_ORIG\")\n\t\t\t\telse\n\t\t\t\t\tuseAltOrig=false\n\t\t\t\t\taltOrigFile=\"\"\n\t\t\t\tfi\n\n\t\t\t\treplacement=\"$fileSet/$baseName\"\n\t\t\t\torig=\"$fileSet/$baseName.orig\"\n\n\t\t\t\t# missing replacement, check all other file sets once more\n\t\t\t\tif [ -f \"$replacement.CHECK_REPLACEMENT\" ]; then\n\t\t\t\t\tstockFileSet=\"$stockFiles/$version\"\n\t\t\t\t\tif $useAltOrig ; then\n\t\t\t\t\t\tstockFile1=\"$stockFileSet$altOrigFile\"\n\t\t\t\t\telse\n\t\t\t\t\t\tstockFile1=\"$stockFileSet$file\"\n\t\t\t\t\tfi\n\t\t\t\t\tfor (( i2 = 0; i2 < $allFileSetsLength; i2++ )); do\n\t\t\t\t\t\tif (( i2 == i1 )); then continue; fi\n\t\t\t\t\t\tIFS=':' read version2 versionNumber2 <<< \"${allFileSets[$i2]}\"\n\t\t\t\t\t\tfileSet2=\"$workingFiles/$version2\"\n\t\t\t\t\t\tif ! [ -e \"$fileSet2\" ] || [ -e \"$fileSet2.INCOMPATIBLE_VERSION\" ]; then continue; fi\n\t\t\t\t\t\treplacement2=\"$fileSet2/$baseName\"\n\t\t\t\t\t\tif ! [ -f \"$replacement2\" ] || [ -L \"$replacement2\" ]; then continue; fi\n\t\t\t\t\t\torig2=\"$fileSet2/$baseName.orig\"\n\t\t\t\t\t\tif ! [ -e \"$orig2\" ]; then continue; fi\n\t\t\t\t\t\tif cmp -s \"$stockFile1\" \"$orig2\" > /dev/null ; then\n\t\t\t\t\t\t\tln -sf \"../$version2/$baseName\" \"$replacement\"\n\t\t\t\t\t\t\trm -f \"$replacement.CHECK_REPLACEMENT\"\n\t\t\t\t\t\t\trm -f \"$orig\"\n\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\tfi\n\t\t\t\t\tdone\n\t\t\t\tfi\n\t\t\t\tif [ -f \"$replacement.CHECK_REPLACEMENT\" ]; then\n\t\t\t\t\tenclosingDir=$( dirname \"$stockFile1\" )\n\t\t\t\t\tif [ -e \"$enclosingDir\" ]; then\n\t\t\t\t\t\tdirName=$( basename \"$enclosingDir\" )\n\t\t\t\t\t\tlogMessage \"ERROR $package \"$dirName\"/$baseName: no replacement for $version\"\n\t\t\t\t\t\t# grab a copy of the orig file to make creating the replacement easier\n\t\t\t\t\t\tif ! [ -e \"$replacement.orig\" ]; then\t\n\t\t\t\t\t\t\tif [ -e \"$stockFile1\" ]; then\n\t\t\t\t\t\t\t\tcp \"$stockFile1\" \"$replacement.orig\"\n\t\t\t\t\t\t\telse\n\t\t\t\t\t\t\t\tlogMessage \"ERROR $package $\"dirName\"/$baseName: no stock file for $version\"\n\t\t\t\t\t\t\tfi\n\t\t\t\t\t\tfi\n\t\t\t\t\tfi\n\t\t\t\tfi\n\n\t\t\t\t# validate sym link\n\t\t\t\tsymLinkReplacement=false\n\t\t\t\tbadLink=false\n\t\t\t\tif [ -L \"$replacement\" ]; then\n\t\t\t\t\tsymLinkReplacement=true\n\t\t\t\t\t# resolive symlink then check to make sure that file is valid\n\t\t\t\t\tlinkedFile=$( realpath \"$replacement\" 2> /dev/null )\n\t\t\t\t\tif [ -z \"$linkedFile\" ]; then\n\t\t\t\t\t\tlogMessage \"ERROR $package: $baseName $version no linked file $linkedFile\"\n\t\t\t\t\t\ttouch \"$replacement.BAD_LINK\"\n\t\t\t\t\telse\n\t\t\t\t\t\tlinkedFileSet=$( dirname \"$linkedFile\" )\n\t\t\t\t\t\tif [ -z \"$linkedFileSet\" ]; then\n\t\t\t\t\t\t\tlogMessage \"ERROR $package: $baseName $version no linked file set\"\n\t\t\t\t\t\t\ttouch \"$replacement.BAD_LINK\"\n\t\t\t\t\t\telif [ -f \"$linkedFileSet/UNUSED_FILE_SET\" ] || [ -f \"$linkedFileSet/INCOMPATIBLE_VERSION\" ]; then\n\t\t\t\t\t\t\tlogMessage \"ERROR $package: $baseName $version links to unused file set $linkedFileSet\"\n\t\t\t\t\t\t\ttouch \"$replacement.BAD_LINK\"\n\t\t\t\t\t\tfi\n\t\t\t\t\tfi\n\t\t\t\tfi\n\n\t\t\t\t# flag file set incomplete\n\t\t\t\tif [ -f \"$replacement.CHECK_REPLACEMENT\" ] || [ -f \"$replacement.NO_ORIG\" ] \\\n\t\t\t\t\t\t|| [ -f \"$replacement.BAD_LINK\" ] ; then\n\t\t\t\t\ttouch \"$fileSet/INCOMPLETE\"\n\t\t\t\tfi\n\t\t\tdone # for file\n\n\t\t\tif [ -f \"$fileSet/INCOMPLETE\" ]; then\n\t\t\t\trm -f \"$fileSet/COMPLETE\"\n\t\t\telse\n\t\t\t\ttouch \"$fileSet/COMPLETE\"\n\t\t\tfi\n\t\t\trm -f \"$fileSet/NEW_FILE_SET\"\n\t\tfi # do final checks for versioned ...\n    done # for i1 (final checks)\n\n\tif (( $packageErrors == 0 )); then\n\t\terrorText=\"no errors \"\n\telse\n\t\terrorText=\"$packageErrors ERRORS \"\n\tfi\n\tif (( $packageWarnings == 0 )); then\n\t\twarningText=\"no warnings\"\n\telse\n\t\twarningText=\"$packageWarnings WARNINGS\"\n\tfi\n\tlogMessage \"$package complete  $errorText $warningText\"\n\n\t# report errors for file sets and patch files\n\tfor (( i1 = 0; i1 < $allFileSetsLength; i1++ )); do\n\t\tIFS=':' read version versionNumber <<< \"${allFileSets[$i1]}\"\n\t\tfileSet=\"$workingFiles/$version\"\n\t\tif ! [ -e \"$fileSet\" ]; then continue; fi\n\n\t\t# if all replacement files are in place, mark the file set COMPLETE\n\t\t#\tso _checkFileSets can skip all checks\n\t\t# COMPLETE tells _checkFileSets to skip all checks and accept the file set as is\n\t\tif [ -f \"$fileSet/INCOMPLETE\" ]; then\n\t\t\trm -f \"$fileSet/COMPLETE\"\n\t\t\tlogMessage \"  INCOMPLETE file set $version\"\n\t\telse\n\t\t\ttouch \"$fileSet/COMPLETE\"\n\t\tfi\n\t\tif [ -f \"$fileSet/INCOMPATIBLE_VERSION\" ]; then\n\t\t\tlogMessage \"  INCOMPATIBLE VERSION $version\"\n\t\tfi\n\t\tif [ -f \"$fileSet/UNUSED_FILE_SET\" ]; then\n\t\t\tlogMessage \"  UNUSED file set $version\"\n\t\tfi\n\tdone\n\tif [ -f \"$patchSourceDir/INCOMPLETE\" ]; then\n\t\tlogMessage \"  MISSING patch files\"\n\tfi\n\n\n\tworkingName=$(basename $workingDirectory)\n\tbackupName=$(basename $backupDirectory)\n\n\tendAction=\"$globalEndAction\"\n\tif [ \"$endAction\" == 'update' ] && (( $packageErrors != 0 )); then\n\t\techo\n\t\tif ! yesNoPrompt \"$package has errors - update anyway (y/n)? \" ; then\n\t\t\tlogMessage \"changes preserved as $workingName\"\n\t\t\tendAction='preserve'\n\t\tfi\n\tfi\n\n\tif [ -z \"$endAction\" ]; then\n\t\techo\n\t\techo \"select to finish:\"\n\t\techo \"  update $package (u)\"\n\t\techo \"  preserve working copy (p)\"\n\t\techo \"  discard working copy (d)\"\n\t\twhile true ; do\n\t\t\tread -p \"choose action from list above (u / p / d): \" response\n\t\t\tcase $response in\n\t\t\t\t[uU]*)\n\t\t\t\t\tif (( $packageErrors == 0 )); then\n\t\t\t\t\t\tendAction='update'\n\t\t\t\t\telse\n\t\t\t\t\t\techo\n\t\t\t\t\t\tif yesNoPrompt \"$package has errors - update anyway (y/n)? \" ;then\n\t\t\t\t\t\t\tlogMessage \"updating $package (with errors)\"\n\t\t\t\t\t\t\tendAction='update'\n\t\t\t\t\t\telse\n\t\t\t\t\t\t\tlogMessage \"changes preserved as $workingName\"\n\t\t\t\t\t\t\tendAction='preserve'\n\t\t\t\t\t\tfi\n\t\t\t\t\tfi\n\t\t\t\t\tbreak\n\t\t\t\t\t;;\n\t\t\t\t[pP]*)\n\t\t\t\t\tendAction='preserve'\n\t\t\t\t\tbreak\n\t\t\t\t\t;;\n\t\t\t\t[dD]*)\n\t\t\t\t\tendAction='delete'\n\t\t\t\t\tbreak\n\t\t\t\t\t;;\n\t\t\t\t*)\n\t\t\tesac\n\t\tdone\n\tfi\n\n\tcase $endAction in\n\t\tpreserve)\n\t\t\tlogMessage \"$package not updated - changes preserved as $workingName\"\n\t\t\t;;\n\t\tdelete)\n\t\t\tlogMessage \"$package not updated - $workingName removed\"\n\t\t\tdeleteNestedDirectories \"$workingDirectory\"\n\t\t\t;;\n\t\tupdate)\n\t\t\tdoBackup=true\n\t\t\tif [ -d \"$backupDirectory\" ] && ! yesNoPrompt \"$backupName exists OVERWRITE it (y/n)? \" ; then\n\t\t\t\tdoBackup=false\n\t\t\tfi\n\t\t\tif $doBackup ; then\n\t\t\t\tlogMessage \"$package: $backupName updated\"\n\t\t\t\tdeleteNestedDirectories \"$package.backup\"\n\t\t\t\tmkdir \"$backupDirectory\"\n\t\t\t\tif [ -d \"$sourceFiles\" ]; then\n\t\t\t\t\tmv \"$sourceFiles\" \"$backupFiles\"\n\t\t\t\tfi\n\t\t\t\tsourceVeLib=\"$sourceDirectory/velib_python\"\n\t\t\t\tbackupVeLib=\"$backupDirectory/velib_python\"\n\t\t\t\tif [ -d \"$sourceVeLib\" ]; then\n\t\t\t\t\tmv -f \"$sourceVeLib\" \"$backupVeLib\"\n\t\t\t\tfi\n\t\t\t\t#### TODO: delayed implementaiton \n\t\t\t\t####if [ -e \"$sourceDirectory/validFirmwareVersions\" ]; then\n\t\t\t\t####\tmv -f \"$sourceDirectory/validFirmwareVersions\" \"$backupDirectory/validFirmwareVersions\"\n\t\t\t\t####fi\n\t\t\telse\n\t\t\t\tlogMessage \"$package: $backupName unchanged\"\n\t\t\tfi\n\n\t\t\tlogMessage \"$package: updating package files\"\n\t\t\tif [ -d \"$workingFiles\" ]; then\n\t\t\t\tdeleteNestedDirectories \"$sourceFiles\"\n\t\t\t\tmv \"$workingFiles\" \"$sourceFiles\"\n\t\t\tfi\n\t\t\tif [ -e \"$sourceDirectory/HelperResources\" ] && [ \"$package\" != \"SetupHelper\" ]; then\n\t\t\t\tlogMessage \"$package: removing HelperResources\"\n\t\t\t\tdeleteNestedDirectories \"$sourceDirectory/HelperResources\"\n\t\t\tfi\n\t\t\tsourceVeLib=\"$sourceDirectory/velib_python\"\n\t\t\tworkingVeLib=\"$backupDirectory/velib_python\"\n\t\t\tif [ -d \"$workingVeLib\" ]; then\n\t\t\t\tmv -f \"$workingVeLib\" \"$sourceVeLib\"\n\t\t\tfi\n\t\t\t#### TODO: delayed implementaiton \n\t\t\t####if [ -e \"$workingDirectory/validFirmwareVersions\" ]; then\n\t\t\t####\tmv -f \"$workingDirectory/validFirmwareVersions\" \"$sourceDirectory/validFirmwareVersions\"\n\t\t\t####fi\n\t\t\tdeleteNestedDirectories \"$workingDirectory\"\n\t\t\t;;\n\t\t*)\n\t\t\tlogMessage \"ERROR: invalid end action $endAction\"\n\tesac\n\tlogMessage \"\"\ndone # for package\n\n# review all file sets and report any that only contain sym links across all packages\n# it would be possile to remove those verions from stock files without loosing any data\n# this check is only done if updating all file sets and there are no errors\nif $doAllPackages && [ \"$totalErrors\" == 0 ]; then\n    for entry in ${stockVersionList[@]} ; do\n\t\tIFS=':' read version versionNumber <<< \"$entry\"\n\t\tlinksOnly=true\n\t\tfor package in $packageList; do\n\t\t\tfileSet=\"$packageRoot/$package/FileSets/$version\"\n\t\t\tif [ ! -e \"$fileSet/LINKS_ONLY\" ]; then\n\t\t\t\tlinksOnly=false\n\t\t\t\tbreak\n\t\t\tfi\n\t\tdone\n\t\tif $linksOnly ; then\n\t\t\tlogMessage \"$version: only links in all packages - stock version could be removed\"\n\t\tfi\n\tdone\nfi\n\nif [ \"$totalErrors\" == 0 ]; then\n    errorText=\"no errors \"\nelse\n    errorText=\"$totalErrors ERRORS \"\nfi\nif [ \"$totalWarnings\" == 0 ]; then\n    warningText=\"no warnings\"\nelse\n    warningText=\"$totalWarnings WARNINGS\"\nfi\n\nlogMessage \"updateFileSets complete  $errorText $warningText\"\n"
  },
  {
    "path": "velib_python/dbusmonitor.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\n## @package dbus_vrm\n# This code takes care of the D-Bus interface (not all of below is implemented yet):\n# - on startup it scans the dbus for services we know. For each known service found, it searches for\n#   objects/paths we know. Everything we find is stored in items{}, and an event is registered: if a\n#   value changes weĺl be notified and can pass that on to our owner. For example the vrmLogger.\n#   we know.\n# - after startup, it continues to monitor the dbus:\n#\t\t1) when services are added we do the same check on that\n#\t\t2) when services are removed, we remove any items that we had that referred to that service\n#\t\t3) if an existing services adds paths we update ourselves as well: on init, we make a\n#\t\t   VeDbusItemImport for a non-, or not yet existing objectpaths as well1\n#\n# Code is used by the vrmLogger, and also the pubsub code. Both are other modules in the dbus_vrm repo.\n\nfrom dbus.mainloop.glib import DBusGMainLoop\nfrom gi.repository import GLib\nimport dbus\nimport dbus.service\nimport inspect\nimport logging\nimport argparse\nimport pprint\nimport traceback\nimport os\nfrom collections import defaultdict\nfrom functools import partial\n\n# our own packages\nfrom ve_utils import exit_on_error, wrap_dbus_value, unwrap_dbus_value, add_name_owner_changed_receiver\nnotfound = object() # For lookups where None is a valid result\n\nlogger = logging.getLogger(__name__)\nlogger.setLevel(logging.INFO)\nclass SystemBus(dbus.bus.BusConnection):\n\tdef __new__(cls):\n\t\treturn dbus.bus.BusConnection.__new__(cls, dbus.bus.BusConnection.TYPE_SYSTEM)\n\nclass SessionBus(dbus.bus.BusConnection):\n\tdef __new__(cls):\n\t\treturn dbus.bus.BusConnection.__new__(cls, dbus.bus.BusConnection.TYPE_SESSION)\n\nclass MonitoredValue(object):\n\tdef __init__(self, value, text, options):\n\t\tsuper(MonitoredValue, self).__init__()\n\t\tself.value = value\n\t\tself.text = text\n\t\tself.options = options\n\n\t# For legacy code, allow treating this as a tuple/list\n\tdef __iter__(self):\n\t\treturn iter((self.value, self.text, self.options))\n\nclass Service(object):\n\tdef __init__(self, id, serviceName, deviceInstance):\n\t\tsuper(Service, self).__init__()\n\t\tself.id = id\n\t\tself.name = serviceName\n\t\tself.paths = {}\n\t\tself._seen = set()\n\t\tself.deviceInstance = deviceInstance\n\n\t# For legacy code, attributes can still be accessed as if keys from a\n\t# dictionary.\n\tdef __setitem__(self, key, value):\n\t\tself.__dict__[key] = value\n\tdef __getitem__(self, key):\n\t\treturn self.__dict__[key]\n\n\tdef set_seen(self, path):\n\t\tself._seen.add(path)\n\n\tdef seen(self, path):\n\t\treturn path in self._seen\n\n\t@property\n\tdef service_class(self):\n\t\treturn '.'.join(self.name.split('.')[:3])\n\nclass DbusMonitor(object):\n\t## Constructor\n\tdef __init__(self, dbusTree, valueChangedCallback=None, deviceAddedCallback=None,\n\t\t\t\t\tdeviceRemovedCallback=None, namespace=\"com.victronenergy\", ignoreServices=[]):\n\t\t# valueChangedCallback is the callback that we call when something has changed.\n\t\t# def value_changed_on_dbus(dbusServiceName, dbusPath, options, changes, deviceInstance):\n\t\t# in which changes is a tuple with GetText() and GetValue()\n\t\tself.valueChangedCallback = valueChangedCallback\n\t\tself.deviceAddedCallback = deviceAddedCallback\n\t\tself.deviceRemovedCallback = deviceRemovedCallback\n\t\tself.dbusTree = dbusTree\n\t\tself.ignoreServices = ignoreServices\n\n\t\t# Lists all tracked services. Stores name, id, device instance, value per path, and whenToLog info\n\t\t# indexed by service name (eg. com.victronenergy.settings).\n\t\tself.servicesByName = {}\n\n\t\t# Same values as self.servicesByName, but indexed by service id (eg. :1.30)\n\t\tself.servicesById = {}\n\n\t\t# Keep track of services by class to speed up calls to get_service_list\n\t\tself.servicesByClass = defaultdict(list)\n\n\t\t# Keep track of any additional watches placed on items\n\t\tself.serviceWatches = defaultdict(list)\n\n\t\t# For a PC, connect to the SessionBus\n\t\t# For a CCGX, connect to the SystemBus\n\t\tself.dbusConn = SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else SystemBus()\n\n\t\t# subscribe to NameOwnerChange for bus connect / disconnect events.\n\t\t# NOTE: this is on a different bus then the one above!\n\t\tstandardBus = (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ \\\n\t\t\telse dbus.SystemBus())\n\n\t\tadd_name_owner_changed_receiver(standardBus, self.dbus_name_owner_changed)\n\n\t\t# Subscribe to PropertiesChanged for all services\n\t\tself.dbusConn.add_signal_receiver(self.handler_value_changes,\n\t\t\tdbus_interface='com.victronenergy.BusItem',\n\t\t\tsignal_name='PropertiesChanged', path_keyword='path',\n\t\t\tsender_keyword='senderId')\n\n\t\t# Subscribe to ItemsChanged for all services\n\t\tself.dbusConn.add_signal_receiver(self.handler_item_changes,\n\t\t\tdbus_interface='com.victronenergy.BusItem',\n\t\t\tsignal_name='ItemsChanged', path='/',\n\t\t\tsender_keyword='senderId')\n\n\t\tlogger.info('===== Search on dbus for services that we will monitor starting... =====')\n\t\tserviceNames = self.dbusConn.list_names()\n\t\tfor serviceName in serviceNames:\n\t\t\tself.scan_dbus_service(serviceName)\n\n\t\tlogger.info('===== Search on dbus for services that we will monitor finished =====')\n\n\t@staticmethod\n\tdef make_service(serviceId, serviceName, deviceInstance):\n\t\t\"\"\" Override this to use a different kind of service object. \"\"\"\n\t\treturn Service(serviceId, serviceName, deviceInstance)\n\n\tdef make_monitor(self, service, path, value, text, options):\n\t\t\"\"\" Override this to do more things with monitoring. \"\"\"\n\t\treturn MonitoredValue(unwrap_dbus_value(value), unwrap_dbus_value(text), options)\n\n\tdef dbus_name_owner_changed(self, name, oldowner, newowner):\n\t\tif not name.startswith(\"com.victronenergy.\"):\n\t\t\treturn\n\n\t\t#decouple, and process in main loop\n\t\tGLib.idle_add(exit_on_error, self._process_name_owner_changed, name, oldowner, newowner)\n\n\tdef _process_name_owner_changed(self, name, oldowner, newowner):\n\t\tif newowner != '':\n\t\t\t# so we found some new service. Check if we can do something with it.\n\t\t\tnewdeviceadded = self.scan_dbus_service(name)\n\t\t\tif newdeviceadded and self.deviceAddedCallback is not None:\n\t\t\t\tself.deviceAddedCallback(name, self.get_device_instance(name))\n\n\t\telif name in self.servicesByName:\n\t\t\t# it disappeared, we need to remove it.\n\t\t\tlogger.info(\"%s disappeared from the dbus. Removing it from our lists\" % name)\n\t\t\tservice = self.servicesByName[name]\n\t\t\tdel self.servicesById[service.id]\n\t\t\tdel self.servicesByName[name]\n\t\t\tfor watch in self.serviceWatches[name]:\n\t\t\t\twatch.remove()\n\t\t\tdel self.serviceWatches[name]\n\t\t\tself.servicesByClass[service.service_class].remove(service)\n\t\t\tif self.deviceRemovedCallback is not None:\n\t\t\t\tself.deviceRemovedCallback(name, service.deviceInstance)\n\n\tdef scan_dbus_service(self, serviceName):\n\t\ttry:\n\t\t\treturn self.scan_dbus_service_inner(serviceName)\n\t\texcept:\n\t\t\tlogger.error(\"Ignoring %s because of error while scanning:\" % (serviceName))\n\t\t\ttraceback.print_exc()\n\t\t\treturn False\n\n\t\t\t# Errors 'org.freedesktop.DBus.Error.ServiceUnknown' and\n\t\t\t# 'org.freedesktop.DBus.Error.Disconnected' seem to happen when the service\n\t\t\t# disappears while its being scanned. Which might happen, but is not really\n\t\t\t# normal either, so letting them go into the logs.\n\n\t# Scans the given dbus service to see if it contains anything interesting for us. If it does, add\n\t# it to our list of monitored D-Bus services.\n\tdef scan_dbus_service_inner(self, serviceName):\n\n\t\t# make it a normal string instead of dbus string\n\t\tserviceName = str(serviceName)\n\n\t\tif (len(self.ignoreServices) != 0 and any(serviceName.startswith(x) for x in self.ignoreServices)):\n\t\t\tlogger.debug(\"Ignoring service %s\" % serviceName)\n\t\t\treturn False\n\n\t\tpaths = self.dbusTree.get('.'.join(serviceName.split('.')[0:3]), None)\n\t\tif paths is None:\n\t\t\tlogger.debug(\"Ignoring service %s, not in the tree\" % serviceName)\n\t\t\treturn False\n\n\t\tlogger.info(\"Found: %s, scanning and storing items\" % serviceName)\n\t\tserviceId = self.dbusConn.get_name_owner(serviceName)\n\n\t\t# we should never be notified to add a D-Bus service that we already have. If this assertion\n\t\t# raises, check process_name_owner_changed, and D-Bus workings.\n\t\tassert serviceName not in self.servicesByName\n\t\tassert serviceId not in self.servicesById\n\n\t\t# Try to fetch everything with a GetItems, then fall back to older\n\t\t# methods if that fails\n\t\ttry:\n\t\t\tvalues = self.dbusConn.call_blocking(serviceName, '/', None, 'GetItems', '', [])\n\t\texcept dbus.exceptions.DBusException:\n\t\t\tlogger.info(\"GetItems failed, trying legacy methods\")\n\t\telse:\n\t\t\treturn self.scan_dbus_service_getitems_done(serviceName, serviceId, values)\n\n\t\tif serviceName == 'com.victronenergy.settings':\n\t\t\tdi = 0\n\t\telif serviceName.startswith('com.victronenergy.vecan.'):\n\t\t\tdi = 0\n\t\telse:\n\t\t\ttry:\n\t\t\t\tdi = self.dbusConn.call_blocking(serviceName,\n\t\t\t\t\t'/DeviceInstance', None, 'GetValue', '', [])\n\t\t\texcept dbus.exceptions.DBusException:\n\t\t\t\tlogger.info(\"       %s was skipped because it has no device instance\" % serviceName)\n\t\t\t\treturn False # Skip it\n\t\t\telse:\n\t\t\t\tdi = int(di)\n\n\t\tlogger.info(\"       %s has device instance %s\" % (serviceName, di))\n\t\tservice = self.make_service(serviceId, serviceName, di)\n\n\t\t# Let's try to fetch everything in one go\n\t\tvalues = {}\n\t\ttexts = {}\n\t\ttry:\n\t\t\tvalues.update(self.dbusConn.call_blocking(serviceName, '/', None, 'GetValue', '', []))\n\t\t\ttexts.update(self.dbusConn.call_blocking(serviceName, '/', None, 'GetText', '', []))\n\t\texcept:\n\t\t\tpass\n\n\t\tfor path, options in paths.items():\n\t\t\t# path will be the D-Bus path: '/Ac/ActiveIn/L1/V'\n\t\t\t# options will be a dictionary: {'code': 'V', 'whenToLog': 'onIntervalAlways'}\n\n\t\t\t# Try to obtain the value we want from our bulk fetch. If we\n\t\t\t# cannot find it there, do an individual query.\n\t\t\tvalue = values.get(path[1:], notfound)\n\t\t\tif value != notfound:\n\t\t\t\tservice.set_seen(path)\n\t\t\ttext = texts.get(path[1:], notfound)\n\t\t\tif value is notfound or text is notfound:\n\t\t\t\ttry:\n\t\t\t\t\tvalue = self.dbusConn.call_blocking(serviceName, path, None, 'GetValue', '', [])\n\t\t\t\t\tservice.set_seen(path)\n\t\t\t\t\ttext = self.dbusConn.call_blocking(serviceName, path, None, 'GetText', '', [])\n\t\t\t\texcept dbus.exceptions.DBusException as e:\n\t\t\t\t\tif e.get_dbus_name() in (\n\t\t\t\t\t\t\t'org.freedesktop.DBus.Error.ServiceUnknown',\n\t\t\t\t\t\t\t'org.freedesktop.DBus.Error.Disconnected'):\n\t\t\t\t\t\traise # This exception will be handled below\n\n\t\t\t\t\t# TODO org.freedesktop.DBus.Error.UnknownMethod really\n\t\t\t\t\t# shouldn't happen but sometimes does.\n\t\t\t\t\tlogger.debug(\"%s %s does not exist (yet)\" % (serviceName, path))\n\t\t\t\t\tvalue = None\n\t\t\t\t\ttext = None\n\n\t\t\tservice.paths[path] = self.make_monitor(service, path, unwrap_dbus_value(value), unwrap_dbus_value(text), options)\n\n\n\t\tlogger.debug(\"Finished scanning and storing items for %s\" % serviceName)\n\n\t\t# Adjust self at the end of the scan, so we don't have an incomplete set of\n\t\t# data if an exception occurs during the scan.\n\t\tself.servicesByName[serviceName] = service\n\t\tself.servicesById[serviceId] = service\n\t\tself.servicesByClass[service.service_class].append(service)\n\n\t\treturn True\n\n\tdef scan_dbus_service_getitems_done(self, serviceName, serviceId, values):\n\t\t# Keeping these exceptions for legacy reasons\n\t\tif serviceName == 'com.victronenergy.settings':\n\t\t\tdi = 0\n\t\telif serviceName.startswith('com.victronenergy.vecan.'):\n\t\t\tdi = 0\n\t\telse:\n\t\t\ttry:\n\t\t\t\tdi = values['/DeviceInstance']['Value']\n\t\t\texcept KeyError:\n\t\t\t\tlogger.info(\"       %s was skipped because it has no device instance\" % serviceName)\n\t\t\t\treturn False\n\t\t\telse:\n\t\t\t\tdi = int(di)\n\n\t\tlogger.info(\"       %s has device instance %s\" % (serviceName, di))\n\t\tservice = self.make_service(serviceId, serviceName, di)\n\n\t\tpaths = self.dbusTree.get('.'.join(serviceName.split('.')[0:3]), {})\n\t\tfor path, options in paths.items():\n\t\t\titem = values.get(path, notfound)\n\t\t\tif item is notfound:\n\t\t\t\tservice.paths[path] = self.make_monitor(service, path, None, None, options)\n\t\t\telse:\n\t\t\t\tservice.set_seen(path)\n\t\t\t\tvalue = item.get('Value', None)\n\t\t\t\ttext = item.get('Text', None)\n\t\t\t\tservice.paths[path] = self.make_monitor(service, path, unwrap_dbus_value(value), unwrap_dbus_value(text), options)\n\n\t\tself.servicesByName[serviceName] = service\n\t\tself.servicesById[serviceId] = service\n\t\tself.servicesByClass[service.service_class].append(service)\n\t\treturn True\n\n\tdef handler_item_changes(self, items, senderId):\n\t\tif not isinstance(items, dict):\n\t\t\treturn\n\n\t\ttry:\n\t\t\tservice = self.servicesById[senderId]\n\t\texcept KeyError:\n\t\t\t# senderId isn't there, which means it hasn't been scanned yet.\n\t\t\treturn\n\n\t\tfor path, changes in items.items():\n\t\t\ttry:\n\t\t\t\tv = unwrap_dbus_value(changes['Value'])\n\t\t\texcept (KeyError, TypeError):\n\t\t\t\tcontinue\n\n\t\t\ttry:\n\t\t\t\tt = changes['Text']\n\t\t\texcept KeyError:\n\t\t\t\tt = str(v)\n\t\t\tself._handler_value_changes(service, path, v, t)\n\n\tdef handler_value_changes(self, changes, path, senderId):\n\t\t# If this properyChange does not involve a value, our work is done.\n\t\tif 'Value' not in changes:\n\t\t\treturn\n\n\t\ttry:\n\t\t\tservice = self.servicesById[senderId]\n\t\texcept KeyError:\n\t\t\t# senderId isn't there, which means it hasn't been scanned yet.\n\t\t\treturn\n\n\t\tv = unwrap_dbus_value(changes['Value'])\n\t\t# Some services don't send Text with their PropertiesChanged events.\n\t\ttry:\n\t\t\tt = changes['Text']\n\t\texcept KeyError:\n\t\t\tt = str(v)\n\t\tself._handler_value_changes(service, path, v, t)\n\n\tdef _handler_value_changes(self, service, path, value, text):\n\t\ttry:\n\t\t\ta = service.paths[path]\n\t\texcept KeyError:\n\t\t\t# path isn't there, which means it hasn't been scanned yet.\n\t\t\treturn\n\n\t\tservice.set_seen(path)\n\n\t\t# First update our store to the new value\n\t\tif a.value == value:\n\t\t\treturn\n\n\t\ta.value = value\n\t\ta.text = text\n\n\t\t# And do the rest of the processing in on the mainloop\n\t\tif self.valueChangedCallback is not None:\n\t\t\tGLib.idle_add(exit_on_error, self._execute_value_changes, service.name, path, {\n\t\t\t\t'Value': value, 'Text': text}, a.options)\n\n\tdef _execute_value_changes(self, serviceName, objectPath, changes, options):\n\t\t# double check that the service still exists, as it might have\n\t\t# disappeared between scheduling-for and executing this function.\n\t\tif serviceName not in self.servicesByName:\n\t\t\treturn\n\n\t\tself.valueChangedCallback(serviceName, objectPath,\n\t\t\toptions, changes, self.get_device_instance(serviceName))\n\n\t# Gets the value for a certain servicename and path\n\t# The default_value is returned when:\n\t# 1. When the service doesn't exist.\n\t# 2. When the path asked for isn't being monitored.\n\t# 3. When the path exists, but has dbus-invalid, ie an empty byte array.\n\t# 4. When the path asked for is being monitored, but doesn't exist for that service.\n\tdef get_value(self, serviceName, objectPath, default_value=None):\n\t\tservice = self.servicesByName.get(serviceName, None)\n\t\tif service is None:\n\t\t\treturn default_value\n\n\t\tvalue = service.paths.get(objectPath, None)\n\t\tif value is None or value.value is None:\n\t\t\treturn default_value\n\n\t\treturn value.value\n\n\t# returns if a dbus exists now, by doing a blocking dbus call.\n\t# Typically seen will be sufficient and doesn't need access to the dbus.\n\tdef exists(self, serviceName, objectPath):\n\t\ttry:\n\t\t\tself.dbusConn.call_blocking(serviceName, objectPath, None, 'GetValue', '', [])\n\t\t\treturn True\n\t\texcept dbus.exceptions.DBusException as e:\n\t\t\treturn False\n\n\t# Returns if there ever was a successful GetValue or valueChanged event.\n\t# Unlike get_value this return True also if the actual value is invalid.\n\t#\n\t# Note: the path might no longer exists anymore, but that doesn't happen in\n\t# practice. If a service really wants to reconfigure itself typically it should\n\t# reconnect to the dbus which causes it to be rescanned and seen will be updated.\n\t# If it is really needed to know if a path still exists, use exists.\n\tdef seen(self, serviceName, objectPath):\n\t\ttry:\n\t\t\treturn self.servicesByName[serviceName].seen(objectPath)\n\t\texcept KeyError:\n\t\t\treturn False\n\n\t# Sets the value for a certain servicename and path, returns the return value of the D-Bus SetValue\n\t# method. If the underlying item does not exist (the service does not exist, or the objectPath was not\n\t# registered) the function will return -1\n\tdef set_value(self, serviceName, objectPath, value):\n\t\t# Check if the D-Bus object referenced by serviceName and objectPath is registered. There is no\n\t\t# necessity to do this, but it is in line with previous implementations which kept VeDbusItemImport\n\t\t# objects for registers items only.\n\t\tservice = self.servicesByName.get(serviceName, None)\n\t\tif service is None:\n\t\t\treturn -1\n\t\tif objectPath not in service.paths:\n\t\t\treturn -1\n\t\t# We do not catch D-Bus exceptions here, because the previous implementation did not do that either.\n\t\treturn self.dbusConn.call_blocking(serviceName, objectPath,\n\t\t\t\t   dbus_interface='com.victronenergy.BusItem',\n\t\t\t\t   method='SetValue', signature=None,\n\t\t\t\t   args=[wrap_dbus_value(value)])\n\n\t# Similar to set_value, but operates asynchronously\n\tdef set_value_async(self, serviceName, objectPath, value,\n\t\t\treply_handler=None, error_handler=None):\n\t\tservice = self.servicesByName.get(serviceName, None)\n\t\tif service is not None:\n\t\t\tif objectPath in service.paths:\n\t\t\t\tself.dbusConn.call_async(serviceName, objectPath,\n\t\t\t\t\tdbus_interface='com.victronenergy.BusItem',\n\t\t\t\t\tmethod='SetValue', signature=None,\n\t\t\t\t\targs=[wrap_dbus_value(value)],\n\t\t\t\t\treply_handler=reply_handler, error_handler=error_handler)\n\t\t\t\treturn\n\n\t\tif error_handler is not None:\n\t\t\terror_handler(TypeError('Service or path not found, '\n\t\t\t\t\t\t'service=%s, path=%s' % (serviceName, objectPath)))\n\n\t# returns a dictionary, keys are the servicenames, value the instances\n\t# optionally use the classfilter to get only a certain type of services, for\n\t# example com.victronenergy.battery.\n\tdef get_service_list(self, classfilter=None):\n\t\tif classfilter is None:\n\t\t\treturn { servicename: service.deviceInstance \\\n\t\t\t\tfor servicename, service in self.servicesByName.items() }\n\n\t\tif classfilter not in self.servicesByClass:\n\t\t\treturn {}\n\n\t\treturn { service.name: service.deviceInstance \\\n\t\t\tfor service in self.servicesByClass[classfilter] }\n\n\tdef get_device_instance(self, serviceName):\n\t\treturn self.servicesByName[serviceName].deviceInstance\n\n\tdef track_value(self, serviceName, objectPath, callback, *args, **kwargs):\n\t\t\"\"\" A DbusMonitor can watch specific service/path combos for changes\n\t\t    so that it is not fully reliant on the global handler_value_changes\n\t\t    in this class. Additional watches are deleted automatically when\n\t\t    the service disappears from dbus. \"\"\"\n\t\tcb = partial(callback, *args, **kwargs)\n\n\t\tdef root_tracker(items):\n\t\t\t# Check if objectPath in dict\n\t\t\ttry:\n\t\t\t\tv = items[objectPath]\n\t\t\t\t_v = unwrap_dbus_value(v['Value'])\n\t\t\texcept (KeyError, TypeError):\n\t\t\t\treturn # not in this dict\n\n\t\t\ttry:\n\t\t\t\tt = v['Text']\n\t\t\texcept KeyError:\n\t\t\t\tcb({'Value': _v })\n\t\t\telse:\n\t\t\t\tcb({'Value': _v, 'Text': t})\n\n\t\t# Track changes on the path, and also on root\n\t\tself.serviceWatches[serviceName].extend((\n\t\t\tself.dbusConn.add_signal_receiver(cb,\n\t\t\t\tdbus_interface='com.victronenergy.BusItem',\n\t\t\t\tsignal_name='PropertiesChanged',\n\t\t\t\tpath=objectPath, bus_name=serviceName),\n\t\t\tself.dbusConn.add_signal_receiver(root_tracker,\n\t\t\t\tdbus_interface='com.victronenergy.BusItem',\n\t\t\t\tsignal_name='ItemsChanged',\n\t\t\t\tpath=\"/\", bus_name=serviceName),\n\t\t))\n\n\n# ====== ALL CODE BELOW THIS LINE IS PURELY FOR DEVELOPING THIS CLASS ======\n\n# Example function that can be used as a starting point to use this code\ndef value_changed_on_dbus(dbusServiceName, dbusPath, dict, changes, deviceInstance):\n\tlogger.debug(\"0 ----------------\")\n\tlogger.debug(\"1 %s%s changed\" % (dbusServiceName, dbusPath))\n\tlogger.debug(\"2 vrm dict     : %s\" % dict)\n\tlogger.debug(\"3 changes-text: %s\" % changes['Text'])\n\tlogger.debug(\"4 changes-value: %s\" % changes['Value'])\n\tlogger.debug(\"5 deviceInstance: %s\" % deviceInstance)\n\tlogger.debug(\"6 - end\")\n\n\ndef nameownerchange(a, b):\n\t# used to find memory leaks in dbusmonitor and VeDbusItemImport\n\timport gc\n\tgc.collect()\n\tobjects = gc.get_objects()\n\tprint (len([o for o in objects if type(o).__name__ == 'VeDbusItemImport']))\n\tprint (len([o for o in objects if type(o).__name__ == 'SignalMatch']))\n\tprint (len(objects))\n\n\ndef print_values(dbusmonitor):\n\ta = dbusmonitor.get_value('wrongservice', '/DbusInvalid', default_value=1000)\n\tb = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/NotInTheMonitorList', default_value=1000)\n\tc = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/DbusInvalid', default_value=1000)\n\td = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/NonExistingButMonitored', default_value=1000)\n\n\tprint (\"All should be 1000: Wrong Service: %s, NotInTheMonitorList: %s, DbusInvalid: %s, NonExistingButMonitored: %s\" % (a, b, c, d))\n\treturn True\n\n# We have a mainloop, but that is just for developing this code. Normally above class & code is used from\n# some other class, such as vrmLogger or the pubsub Implementation.\ndef main():\n\t# Init logging\n\tlogging.basicConfig(level=logging.DEBUG)\n\tlogger.info(__file__ + \" is starting up\")\n\n\t# Have a mainloop, so we can send/receive asynchronous calls to and from dbus\n\tDBusGMainLoop(set_as_default=True)\n\n\timport os\n\timport sys\n\tsys.path.insert(1, os.path.join(os.path.dirname(__file__), '../../'))\n\n\tdummy = {'code': None, 'whenToLog': 'configChange', 'accessLevel': None}\n\tmonitorlist = {'com.victronenergy.dummyservice': {\n\t\t\t\t'/Connected': dummy,\n\t\t\t\t'/ProductName': dummy,\n\t\t\t\t'/Mgmt/Connection': dummy,\n\t\t\t\t'/Dc/0/Voltage': dummy,\n\t\t\t\t'/Dc/0/Current': dummy,\n\t\t\t\t'/Dc/0/Temperature': dummy,\n\t\t\t\t'/Load/I': dummy,\n\t\t\t\t'/FirmwareVersion': dummy,\n\t\t\t\t'/DbusInvalid': dummy,\n\t\t\t\t'/NonExistingButMonitored': dummy}}\n\n\td = DbusMonitor(monitorlist, value_changed_on_dbus,\n\t\tdeviceAddedCallback=nameownerchange, deviceRemovedCallback=nameownerchange)\n\n\tGLib.timeout_add(1000, print_values, d)\n\n\t# Start and run the mainloop\n\tlogger.info(\"Starting mainloop, responding on only events\")\n\tmainloop = GLib.MainLoop()\n\tmainloop.run()\n\nif __name__ == \"__main__\":\n\tmain()\n"
  },
  {
    "path": "velib_python/oldestVersion",
    "content": "v3.40~39\n"
  },
  {
    "path": "velib_python/settingsdevice.py",
    "content": "import dbus\nimport logging\nimport time\nfrom functools import partial\n\n# Local imports\nfrom vedbus import VeDbusItemImport\n\n## Indexes for the setting dictonary.\nPATH = 0\nVALUE = 1\nMINIMUM = 2\nMAXIMUM = 3\nSILENT = 4\n\n## The Settings Device class.\n# Used by python programs, such as the vrm-logger, to read and write settings they\n# need to store on disk. And since these settings might be changed from a different\n# source, such as the GUI, the program can pass an eventCallback that will be called\n# as soon as some setting is changed.\n#\n# The settings are stored in flash via the com.victronenergy.settings service on dbus.\n# See https://github.com/victronenergy/localsettings for more info.\n#\n# If there are settings in de supportSettings list which are not yet on the dbus, \n# and therefore not yet in the xml file, they will be added through the dbus-addSetting\n# interface of com.victronenergy.settings.\nclass SettingsDevice(object):\n\t## The constructor processes the tree of dbus-items.\n\t# @param bus the system-dbus object\n\t# @param name the dbus-service-name of the settings dbus service, 'com.victronenergy.settings'\n\t# @param supportedSettings dictionary with all setting-names, and their defaultvalue, min, max and whether\n\t# the setting is silent. The 'silent' entry is optional. If set to true, no changes in the setting will\n\t# be logged by localsettings.\n\t# @param eventCallback function that will be called on changes on any of these settings\n\t# @param timeout Maximum interval to wait for localsettings. An exception is thrown at the end of the\n\t# interval if the localsettings D-Bus service has not appeared yet.\n\tdef __init__(self, bus, supportedSettings, eventCallback, name='com.victronenergy.settings', timeout=0):\n\t\tlogging.debug(\"===== Settings device init starting... =====\")\n\t\tself._bus = bus\n\t\tself._dbus_name = name\n\t\tself._eventCallback = eventCallback\n\t\tself._values = {} # stored the values, used to pass the old value along on a setting change\n\t\tself._settings = {}\n\n\t\tcount = 0\n\t\twhile True:\n\t\t\tif 'com.victronenergy.settings' in self._bus.list_names():\n\t\t\t\tbreak\n\t\t\tif count == timeout:\n\t\t\t\traise Exception(\"The settings service com.victronenergy.settings does not exist!\")\n\t\t\tcount += 1\n\t\t\tlogging.info('waiting for settings')\n\t\t\ttime.sleep(1)\n\n\t\t# Add the items.\n\t\tself.addSettings(supportedSettings)\n\n\t\tlogging.debug(\"===== Settings device init finished =====\")\n\n\tdef addSettings(self, settings):\n\t\tfor setting, options in settings.items():\n\t\t\tsilent = len(options) > SILENT and options[SILENT]\n\t\t\tbusitem = self.addSetting(options[PATH], options[VALUE],\n\t\t\t\toptions[MINIMUM], options[MAXIMUM], silent, callback=partial(self.handleChangedSetting, setting))\n\t\t\tself._settings[setting] = busitem\n\t\t\tself._values[setting] = busitem.get_value()\n\n\tdef addSetting(self, path, value, _min, _max, silent=False, callback=None):\n\t\tbusitem = VeDbusItemImport(self._bus, self._dbus_name, path, callback)\n\t\tif busitem.exists and (value, _min, _max, silent) == busitem._proxy.GetAttributes():\n\t\t\tlogging.debug(\"Setting %s found\" % path)\n\t\telse:\n\t\t\tlogging.info(\"Setting %s does not exist yet or must be adjusted\" % path)\n\n\t\t\t# Prepare to add the setting. Most dbus types extend the python\n\t\t\t# type so it is only necessary to additionally test for Int64.\n\t\t\tif isinstance(value, (int, dbus.Int64)):\n\t\t\t\titemType = 'i'\n\t\t\telif isinstance(value, float):\n\t\t\t\titemType = 'f'\n\t\t\telse:\n\t\t\t\titemType = 's'\n\n\t\t\t# Add the setting\n\t\t\t# TODO, make an object that inherits VeDbusItemImport, and complete the D-Bus settingsitem interface\n\t\t\tsettings_item = VeDbusItemImport(self._bus, self._dbus_name, '/Settings', createsignal=False)\n\t\t\tsetting_path = path.replace('/Settings/', '', 1)\n\t\t\tif silent:\n\t\t\t\tsettings_item._proxy.AddSilentSetting('', setting_path, value, itemType, _min, _max)\n\t\t\telse:\n\t\t\t\tsettings_item._proxy.AddSetting('', setting_path, value, itemType, _min, _max)\n\n\t\t\tbusitem = VeDbusItemImport(self._bus, self._dbus_name, path, callback)\n\n\t\treturn busitem\n\n\tdef handleChangedSetting(self, setting, servicename, path, changes):\n\t\toldvalue = self._values[setting] if setting in self._values else None\n\t\tself._values[setting] = changes['Value']\n\n\t\tif self._eventCallback is None:\n\t\t\treturn\n\n\t\tself._eventCallback(setting, oldvalue, changes['Value'])\n\n\tdef setDefault(self, path):\n                item = VeDbusItemImport(self._bus, self._dbus_name, path, createsignal=False)\n                item.set_default()\n\n\tdef __getitem__(self, setting):\n\t\treturn self._settings[setting].get_value()\n\n\tdef __setitem__(self, setting, newvalue):\n\t\tresult = self._settings[setting].set_value(newvalue)\n\t\tif result != 0:\n\t\t\t# Trying to make some false change to our own settings? How dumb!\n\t\t\tassert False\n"
  },
  {
    "path": "velib_python/ve_utils.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\nimport sys\nfrom traceback import print_exc\nfrom os import _exit as os_exit\nfrom os import statvfs\nfrom subprocess import check_output, CalledProcessError\nimport logging\nimport dbus\nlogger = logging.getLogger(__name__)\n\nVEDBUS_INVALID = dbus.Array([], signature=dbus.Signature('i'), variant_level=1)\n\nclass NoVrmPortalIdError(Exception):\n\tpass\n\n# Use this function to make sure the code quits on an unexpected exception. Make sure to use it\n# when using GLib.idle_add and also GLib.timeout_add.\n# Without this, the code will just keep running, since GLib does not stop the mainloop on an\n# exception.\n# Example: GLib.idle_add(exit_on_error, myfunc, arg1, arg2)\ndef exit_on_error(func, *args, **kwargs):\n\ttry:\n\t\treturn func(*args, **kwargs)\n\texcept:\n\t\ttry:\n\t\t\tprint ('exit_on_error: there was an exception. Printing stacktrace will be tried and then exit')\n\t\t\tprint_exc()\n\t\texcept:\n\t\t\tpass\n\n\t\t# sys.exit() is not used, since that throws an exception, which does not lead to a program\n\t\t# halt when used in a dbus callback, see connection.py in the Python/Dbus libraries, line 230.\n\t\tos_exit(1)\n\n\n__vrm_portal_id = None\ndef get_vrm_portal_id():\n\t# The original definition of the VRM Portal ID is that it is the mac\n\t# address of the onboard- ethernet port (eth0), stripped from its colons\n\t# (:) and lower case. This may however differ between platforms. On Venus\n\t# the task is therefore deferred to /sbin/get-unique-id so that a\n\t# platform specific method can be easily defined.\n\t#\n\t# If /sbin/get-unique-id does not exist, then use the ethernet address\n\t# of eth0. This also handles the case where velib_python is used as a\n\t# package install on a Raspberry Pi.\n\t#\n\t# On a Linux host where the network interface may not be eth0, you can set\n\t# the VRM_IFACE environment variable to the correct name.\n\n\tglobal __vrm_portal_id\n\n\tif __vrm_portal_id:\n\t\treturn __vrm_portal_id\n\n\tportal_id = None\n\n\t# First try the method that works if we don't have a data partition. This\n\t# will fail when the current user is not root.\n\ttry:\n\t\tportal_id = check_output(\"/sbin/get-unique-id\").decode(\"utf-8\", \"ignore\").strip()\n\t\tif not portal_id:\n\t\t\traise NoVrmPortalIdError(\"get-unique-id returned blank\")\n\t\t__vrm_portal_id = portal_id\n\t\treturn portal_id\n\texcept CalledProcessError:\n\t\t# get-unique-id returned non-zero\n\t\traise NoVrmPortalIdError(\"get-unique-id returned non-zero\")\n\texcept OSError:\n\t\t# File doesn't exist, use fallback\n\t\tpass\n\n\t# Fall back to getting our id using a syscall. Assume we are on linux.\n\t# Allow the user to override what interface is used using an environment\n\t# variable.\n\timport fcntl, socket, struct, os\n\n\tiface = os.environ.get('VRM_IFACE', 'eth0').encode('ascii')\n\ts = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)\n\ttry:\n\t\tinfo = fcntl.ioctl(s.fileno(), 0x8927,  struct.pack('256s', iface[:15]))\n\texcept IOError:\n\t\traise NoVrmPortalIdError(\"ioctl failed for eth0\")\n\n\t__vrm_portal_id = info[18:24].hex()\n\treturn __vrm_portal_id\n\n\n# See VE.Can registers - public.docx for definition of this conversion\ndef convert_vreg_version_to_readable(version):\n\tdef str_to_arr(x, length):\n\t\ta = []\n\t\tfor i in range(0, len(x), length):\n\t\t\ta.append(x[i:i+length])\n\t\treturn a\n\n\tx = \"%x\" % version\n\tx = x.upper()\n\n\tif len(x) == 5 or len(x) == 3 or len(x) == 1:\n\t\tx = '0' + x\n\n\ta = str_to_arr(x, 2);\n\n\t# remove the first 00 if there are three bytes and it is 00\n\tif len(a) == 3 and a[0] == '00':\n\t\ta.remove(0);\n\n\t# if we have two or three bytes now, and the first character is a 0, remove it\n\tif len(a) >= 2 and a[0][0:1] == '0':\n\t\ta[0] = a[0][1];\n\n\tresult = ''\n\tfor item in a:\n\t\tresult += ('.' if result != '' else '') + item\n\n\n\tresult = 'v' + result\n\n\treturn result\n\n\ndef get_free_space(path):\n\tresult = -1\n\n\ttry:\n\t\ts = statvfs(path)\n\t\tresult = s.f_frsize * s.f_bavail     # Number of free bytes that ordinary users\n\texcept Exception as ex:\n\t\tlogger.info(\"Error while retrieving free space for path %s: %s\" % (path, ex))\n\n\treturn result\n\n\ndef _get_sysfs_machine_name():\n\ttry:\n\t\twith open('/sys/firmware/devicetree/base/model', 'r') as f:\n\t\t\treturn f.read().rstrip('\\x00')\n\texcept IOError:\n\t\tpass\n\n\treturn None\n\n# Returns None if it cannot find a machine name. Otherwise returns the string\n# containing the name\ndef get_machine_name():\n\t# First try calling the venus utility script\n\ttry:\n\t\treturn check_output(\"/usr/bin/product-name\").strip().decode('UTF-8')\n\texcept (CalledProcessError, OSError):\n\t\tpass\n\n\t# Fall back to sysfs\n\tname = _get_sysfs_machine_name()\n\tif name is not None:\n\t\treturn name\n\n\t# Fall back to venus build machine name\n\ttry:\n\t\twith open('/etc/venus/machine', 'r', encoding='UTF-8') as f:\n\t\t\treturn f.read().strip()\n\texcept IOError:\n\t\tpass\n\n\treturn None\n\n\ndef get_product_id():\n\t\"\"\" Find the machine ID and return it. \"\"\"\n\n\t# First try calling the venus utility script\n\ttry:\n\t\treturn check_output(\"/usr/bin/product-id\").strip().decode('UTF-8')\n\texcept (CalledProcessError, OSError):\n\t\tpass\n\n\t# Fall back machine name mechanism\n\tname = _get_sysfs_machine_name()\n\treturn {\n\t\t'Color Control GX': 'C001',\n\t\t'Venus GX': 'C002',\n\t\t'Octo GX': 'C006',\n\t\t'EasySolar-II': 'C007',\n\t\t'MultiPlus-II': 'C008',\n\t\t'Maxi GX': 'C009',\n\t\t'Cerbo GX': 'C00A'\n\t}.get(name, 'C003') # C003 is Generic\n\n\n# Returns False if it cannot open the file. Otherwise returns its rstripped contents\ndef read_file(path):\n\tcontent = False\n\n\ttry:\n\t\twith open(path, 'r') as f:\n\t\t\tcontent = f.read().rstrip()\n\texcept Exception as ex:\n\t\tlogger.debug(\"Error while reading %s: %s\" % (path, ex))\n\n\treturn content\n\n\ndef wrap_dbus_value(value):\n\tif value is None:\n\t\treturn VEDBUS_INVALID\n\tif isinstance(value, float):\n\t\treturn dbus.Double(value, variant_level=1)\n\tif isinstance(value, bool):\n\t\treturn dbus.Boolean(value, variant_level=1)\n\tif isinstance(value, int):\n\t\ttry:\n\t\t\treturn dbus.Int32(value, variant_level=1)\n\t\texcept OverflowError:\n\t\t\treturn dbus.Int64(value, variant_level=1)\n\tif isinstance(value, str):\n\t\treturn dbus.String(value, variant_level=1)\n\tif isinstance(value, list):\n\t\tif len(value) == 0:\n\t\t\t# If the list is empty we cannot infer the type of the contents. So assume unsigned integer.\n\t\t\t# A (signed) integer is dangerous, because an empty list of signed integers is used to encode\n\t\t\t# an invalid value.\n\t\t\treturn dbus.Array([], signature=dbus.Signature('u'), variant_level=1)\n\t\treturn dbus.Array([wrap_dbus_value(x) for x in value], variant_level=1)\n\tif isinstance(value, dict):\n\t\t# Wrapping the keys of the dictionary causes D-Bus errors like:\n\t\t# 'arguments to dbus_message_iter_open_container() were incorrect,\n\t\t# assertion \"(type == DBUS_TYPE_ARRAY && contained_signature &&\n\t\t# *contained_signature == DBUS_DICT_ENTRY_BEGIN_CHAR) || (contained_signature == NULL ||\n\t\t# _dbus_check_is_valid_signature (contained_signature))\" failed in file ...'\n\t\treturn dbus.Dictionary({(k, wrap_dbus_value(v)) for k, v in value.items()}, variant_level=1)\n\treturn value\n\n\ndbus_int_types = (dbus.Int32, dbus.UInt32, dbus.Byte, dbus.Int16, dbus.UInt16, dbus.UInt32, dbus.Int64, dbus.UInt64)\n\n\ndef unwrap_dbus_value(val):\n\t\"\"\"Converts D-Bus values back to the original type. For example if val is of type DBus.Double,\n\ta float will be returned.\"\"\"\n\tif isinstance(val, dbus_int_types):\n\t\treturn int(val)\n\tif isinstance(val, dbus.Double):\n\t\treturn float(val)\n\tif isinstance(val, dbus.Array):\n\t\tv = [unwrap_dbus_value(x) for x in val]\n\t\treturn None if len(v) == 0 else v\n\tif isinstance(val, (dbus.Signature, dbus.String)):\n\t\treturn str(val)\n\t# Python has no byte type, so we convert to an integer.\n\tif isinstance(val, dbus.Byte):\n\t\treturn int(val)\n\tif isinstance(val, dbus.ByteArray):\n\t\treturn \"\".join([bytes(x) for x in val])\n\tif isinstance(val, (list, tuple)):\n\t\treturn [unwrap_dbus_value(x) for x in val]\n\tif isinstance(val, (dbus.Dictionary, dict)):\n\t\t# Do not unwrap the keys, see comment in wrap_dbus_value\n\t\treturn dict([(x, unwrap_dbus_value(y)) for x, y in val.items()])\n\tif isinstance(val, dbus.Boolean):\n\t\treturn bool(val)\n\treturn val\n\n# When supported, only name owner changes for the the given namespace are reported. This\n# prevents spending cpu time at irrelevant changes, like scripts accessing the bus temporarily.\ndef add_name_owner_changed_receiver(dbus, name_owner_changed, namespace=\"com.victronenergy\"):\n\t# support for arg0namespace is submitted upstream, but not included at the time of\n\t# writing, Venus OS does support it, so try if it works.\n\tif namespace is None:\n\t\tdbus.add_signal_receiver(name_owner_changed, signal_name='NameOwnerChanged')\n\telse:\n\t\ttry:\n\t\t\tdbus.add_signal_receiver(name_owner_changed,\n\t\t\t\tsignal_name='NameOwnerChanged', arg0namespace=namespace)\n\t\texcept TypeError:\n\t\t\tdbus.add_signal_receiver(name_owner_changed, signal_name='NameOwnerChanged')\n"
  },
  {
    "path": "velib_python/vedbus.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\nimport dbus.service\nimport logging\nimport traceback\nimport os\nimport weakref\nfrom collections import defaultdict\nfrom ve_utils import wrap_dbus_value, unwrap_dbus_value\n\n# vedbus contains three classes:\n# VeDbusItemImport -> use this to read data from the dbus, ie import\n# VeDbusItemExport -> use this to export data to the dbus (one value)\n# VeDbusService -> use that to create a service and export several values to the dbus\n\n# Code for VeDbusItemImport is copied from busitem.py and thereafter modified.\n# All projects that used busitem.py need to migrate to this package. And some\n# projects used to define there own equivalent of VeDbusItemExport. Better to\n# use VeDbusItemExport, or even better the VeDbusService class that does it all for you.\n\n# TODOS\n# 1 check for datatypes, it works now, but not sure if all is compliant with\n#\tcom.victronenergy.BusItem interface definition. See also the files in\n#\ttests_and_examples. And see 'if type(v) == dbus.Byte:' on line 102. Perhaps\n#\tsomething similar should also be done in VeDbusBusItemExport?\n# 2 Shouldn't VeDbusBusItemExport inherit dbus.service.Object?\n# 7 Make hard rules for services exporting data to the D-Bus, in order to make tracking\n#   changes possible. Does everybody first invalidate its data before leaving the bus?\n#   And what about before taking one object away from the bus, instead of taking the\n#   whole service offline?\n#   They should! And after taking one value away, do we need to know that someone left\n#   the bus? Or we just keep that value in invalidated for ever? Result is that we can't\n#   see the difference anymore between an invalidated value and a value that was first on\n#   the bus and later not anymore. See comments above VeDbusItemImport as well.\n# 9 there are probably more todos in the code below.\n\n# Some thoughts with regards to the data types:\n#\n#   Text from: http://dbus.freedesktop.org/doc/dbus-python/doc/tutorial.html#data-types\n#   ---\n#   Variants are represented by setting the variant_level keyword argument in the\n#   constructor of any D-Bus data type to a value greater than 0 (variant_level 1\n#   means a variant containing some other data type, variant_level 2 means a variant\n#   containing a variant containing some other data type, and so on). If a non-variant\n#   is passed as an argument but introspection indicates that a variant is expected,\n#   it'll automatically be wrapped in a variant.\n#   ---\n#\n#   Also the different dbus datatypes, such as dbus.Int32, and dbus.UInt32 are a subclass\n#   of Python int. dbus.String is a subclass of Python standard class unicode, etcetera\n#\n#   So all together that explains why we don't need to explicitly convert back and forth\n#   between the dbus datatypes and the standard python datatypes. Note that all datatypes\n#   in python are objects. Even an int is an object.\n\n#   The signature of a variant is 'v'.\n\n# Export ourselves as a D-Bus service.\nclass VeDbusService(object):\n\tdef __init__(self, servicename, bus=None, register=True):\n\t\t# dict containing the VeDbusItemExport objects, with their path as the key.\n\t\tself._dbusobjects = {}\n\t\tself._dbusnodes = {}\n\t\tself._ratelimiters = []\n\t\tself._dbusname = None\n\t\tself.name = servicename\n\n\t\t# dict containing the onchange callbacks, for each object. Object path is the key\n\t\tself._onchangecallbacks = {}\n\n\t\t# Connect to session bus whenever present, else use the system bus\n\t\tself._dbusconn = bus or (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else dbus.SystemBus())\n\n\t\t# make the dbus connection available to outside, could make this a true property instead, but ach..\n\t\tself.dbusconn = self._dbusconn\n\n\t\t# Add the root item that will return all items as a tree\n\t\tself._dbusnodes['/'] = VeDbusRootExport(self._dbusconn, '/', self)\n\n\t\t# Immediately register the service unless requested not to\n\t\tif register:\n\t\t\tself.register()\n\n\tdef register(self):\n\t\t# Register ourselves on the dbus, trigger an error if already in use (do_not_queue)\n\t\tself._dbusname = dbus.service.BusName(self.name, self._dbusconn, do_not_queue=True)\n\t\tlogging.info(\"registered ourselves on D-Bus as %s\" % self.name)\n\n\t# To force immediate deregistering of this dbus service and all its object paths, explicitly\n\t# call __del__().\n\tdef __del__(self):\n\t\tfor node in list(self._dbusnodes.values()):\n\t\t\tnode.__del__()\n\t\tself._dbusnodes.clear()\n\t\tfor item in list(self._dbusobjects.values()):\n\t\t\titem.__del__()\n\t\tself._dbusobjects.clear()\n\t\tif self._dbusname:\n\t\t\tself._dbusname.__del__()  # Forces call to self._bus.release_name(self._name), see source code\n\t\tself._dbusname = None\n\n\tdef get_name(self):\n\t\treturn self._dbusname.get_name()\n\n\t# @param callbackonchange\tfunction that will be called when this value is changed. First parameter will\n\t#\t\t\t\t\t\t\tbe the path of the object, second the new value. This callback should return\n\t#\t\t\t\t\t\t\tTrue to accept the change, False to reject it.\n\tdef add_path(self, path, value, description=\"\", writeable=False,\n\t\t\t\t\tonchangecallback=None, gettextcallback=None, valuetype=None, itemtype=None):\n\n\t\tif onchangecallback is not None:\n\t\t\tself._onchangecallbacks[path] = onchangecallback\n\n\t\titemtype = itemtype or VeDbusItemExport\n\t\titem = itemtype(self._dbusconn, path, value, description, writeable,\n\t\t\t\tself._value_changed, gettextcallback, deletecallback=self._item_deleted, valuetype=valuetype)\n\n\t\tspl = path.split('/')\n\t\tfor i in range(2, len(spl)):\n\t\t\tsubPath = '/'.join(spl[:i])\n\t\t\tif subPath not in self._dbusnodes and subPath not in self._dbusobjects:\n\t\t\t\tself._dbusnodes[subPath] = VeDbusTreeExport(self._dbusconn, subPath, self)\n\t\tself._dbusobjects[path] = item\n\t\tlogging.debug('added %s with start value %s. Writeable is %s' % (path, value, writeable))\n\t\treturn item\n\n\t# Add the mandatory paths, as per victron dbus api doc\n\tdef add_mandatory_paths(self, processname, processversion, connection,\n\t\t\tdeviceinstance, productid, productname, firmwareversion, hardwareversion, connected):\n\t\tself.add_path('/Mgmt/ProcessName', processname)\n\t\tself.add_path('/Mgmt/ProcessVersion', processversion)\n\t\tself.add_path('/Mgmt/Connection', connection)\n\n\t\t# Create rest of the mandatory objects\n\t\tself.add_path('/DeviceInstance', deviceinstance)\n\t\tself.add_path('/ProductId', productid)\n\t\tself.add_path('/ProductName', productname)\n\t\tself.add_path('/FirmwareVersion', firmwareversion)\n\t\tself.add_path('/HardwareVersion', hardwareversion)\n\t\tself.add_path('/Connected', connected)\n\n\t# Callback function that is called from the VeDbusItemExport objects when a value changes. This function\n\t# maps the change-request to the onchangecallback given to us for this specific path.\n\tdef _value_changed(self, path, newvalue):\n\t\tif path not in self._onchangecallbacks:\n\t\t\treturn True\n\n\t\treturn self._onchangecallbacks[path](path, newvalue)\n\n\tdef _item_deleted(self, path):\n\t\tself._dbusobjects.pop(path)\n\t\tfor np in list(self._dbusnodes.keys()):\n\t\t\tif np != '/':\n\t\t\t\tfor ip in self._dbusobjects:\n\t\t\t\t\tif ip.startswith(np + '/'):\n\t\t\t\t\t\tbreak\n\t\t\t\telse:\n\t\t\t\t\tself._dbusnodes[np].__del__()\n\t\t\t\t\tself._dbusnodes.pop(np)\n\n\tdef __getitem__(self, path):\n\t\treturn self._dbusobjects[path].local_get_value()\n\n\tdef __setitem__(self, path, newvalue):\n\t\tself._dbusobjects[path].local_set_value(newvalue)\n\n\tdef __delitem__(self, path):\n\t\tself._dbusobjects[path].__del__()  # Invalidates and then removes the object path\n\t\tassert path not in self._dbusobjects\n\n\tdef __contains__(self, path):\n\t\treturn path in self._dbusobjects\n\n\tdef __enter__(self):\n\t\tl = ServiceContext(self)\n\t\tself._ratelimiters.append(l)\n\t\treturn l\n\n\tdef __exit__(self, *exc):\n\t\t# pop off the top one and flush it. If with statements are nested\n\t\t# then each exit flushes its own part.\n\t\tif self._ratelimiters:\n\t\t\tself._ratelimiters.pop().flush()\n\nclass ServiceContext(object):\n\tdef __init__(self, parent):\n\t\tself.parent = parent\n\t\tself.changes = {}\n\n\tdef __contains__(self, path):\n\t\treturn path in self.parent\n\n\tdef __getitem__(self, path):\n\t\treturn self.parent[path]\n\n\tdef __setitem__(self, path, newvalue):\n\t\tc = self.parent._dbusobjects[path]._local_set_value(newvalue)\n\t\tif c is not None:\n\t\t\tself.changes[path] = c\n\n\tdef __delitem__(self, path):\n\t\tif path in self.changes:\n\t\t\tdel self.changes[path]\n\t\tdel self.parent[path]\n\n\tdef flush(self):\n\t\tif self.changes:\n\t\t\tself.parent._dbusnodes['/'].ItemsChanged(self.changes)\n\t\t\tself.changes.clear()\n\n\tdef add_path(self, path, value, *args, **kwargs):\n\t\tself.parent.add_path(path, value, *args, **kwargs)\n\t\tself.changes[path] = {\n\t\t\t'Value': wrap_dbus_value(value),\n\t\t\t'Text': self.parent._dbusobjects[path].GetText()\n\t\t}\n\n\tdef del_tree(self, root):\n\t\troot = root.rstrip('/')\n\t\tfor p in list(self.parent._dbusobjects.keys()):\n\t\t\tif p == root or p.startswith(root + '/'):\n\t\t\t\tself[p] = None\n\t\t\t\tself.parent._dbusobjects[p].__del__()\n\n\tdef get_name(self):\n\t\treturn self.parent.get_name()\n\nclass TrackerDict(defaultdict):\n\t\"\"\" Same as defaultdict, but passes the key to default_factory. \"\"\"\n\tdef __missing__(self, key):\n\t\tself[key] = x = self.default_factory(key)\n\t\treturn x\n\nclass VeDbusRootTracker(object):\n\t\"\"\" This tracks the root of a dbus path and listens for PropertiesChanged\n\t    signals. When a signal arrives, parse it and unpack the key/value changes\n\t    into traditional events, then pass it to the original eventCallback\n\t    method. \"\"\"\n\tdef __init__(self, bus, serviceName):\n\t\tself.importers = defaultdict(weakref.WeakSet)\n\t\tself.serviceName = serviceName\n\t\tself._match = bus.get_object(serviceName, '/', introspect=False).connect_to_signal(\n\t\t\t\"ItemsChanged\", weak_functor(self._items_changed_handler))\n\n\tdef __del__(self):\n\t\tself._match.remove()\n\t\tself._match = None\n\n\tdef add(self, i):\n\t\tself.importers[i.path].add(i)\n\n\tdef _items_changed_handler(self, items):\n\t\tif not isinstance(items, dict):\n\t\t\treturn\n\n\t\tfor path, changes in items.items():\n\t\t\ttry:\n\t\t\t\tv = changes['Value']\n\t\t\texcept KeyError:\n\t\t\t\tcontinue\n\n\t\t\ttry:\n\t\t\t\tt = changes['Text']\n\t\t\texcept KeyError:\n\t\t\t\tt = str(unwrap_dbus_value(v))\n\n\t\t\tfor i in self.importers.get(path, ()):\n\t\t\t\ti._properties_changed_handler({'Value': v, 'Text': t})\n\n\"\"\"\nImporting basics:\n\t- If when we power up, the D-Bus service does not exist, or it does exist and the path does not\n\t  yet exist, still subscribe to a signal: as soon as it comes online it will send a signal with its\n\t  initial value, which VeDbusItemImport will receive and use to update local cache. And, when set,\n\t  call the eventCallback.\n\t- If when we power up, save it\n\t- When using get_value, know that there is no difference between services (or object paths) that don't\n\t  exist and paths that are invalid (= empty array, see above). Both will return None. In case you do\n\t  really want to know ifa path exists or not, use the exists property.\n\t- When a D-Bus service leaves the D-Bus, it will first invalidate all its values, and send signals\n\t  with that update, and only then leave the D-Bus. (or do we need to subscribe to the NameOwnerChanged-\n\t  signal!?!) To be discussed and make sure. Not really urgent, since all existing code that uses this\n\t  class already subscribes to the NameOwnerChanged signal, and subsequently removes instances of this\n\t  class.\n\nRead when using this class:\nNote that when a service leaves that D-Bus without invalidating all its exported objects first, for\nexample because it is killed, VeDbusItemImport doesn't have a clue. So when using VeDbusItemImport,\nmake sure to also subscribe to the NamerOwnerChanged signal on bus-level. Or just use dbusmonitor,\nbecause that takes care of all of that for you.\n\"\"\"\nclass VeDbusItemImport(object):\n\tdef __new__(cls, bus, serviceName, path, eventCallback=None, createsignal=True):\n\t\tinstance = object.__new__(cls)\n\n\t\t# If signal tracking should be done, also add to root tracker\n\t\tif createsignal:\n\t\t\tif \"_roots\" not in cls.__dict__:\n\t\t\t\tcls._roots = TrackerDict(lambda k: VeDbusRootTracker(bus, k))\n\n\t\treturn instance\n\n\t## Constructor\n\t# @param bus\t\t\tthe bus-object (SESSION or SYSTEM).\n\t# @param serviceName\tthe dbus-service-name (string), for example 'com.victronenergy.battery.ttyO1'\n\t# @param path\t\t\tthe object-path, for example '/Dc/V'\n\t# @param eventCallback\tfunction that you want to be called on a value change\n\t# @param createSignal   only set this to False if you use this function to one time read a value. When\n\t#\t\t\t\t\t\tleaving it to True, make sure to also subscribe to the NameOwnerChanged signal\n\t#\t\t\t\t\t\telsewhere. See also note some 15 lines up.\n\tdef __init__(self, bus, serviceName, path, eventCallback=None, createsignal=True):\n\t\t# TODO: is it necessary to store _serviceName and _path? Isn't it\n\t\t# stored in the bus_getobjectsomewhere?\n\t\tself._serviceName = serviceName\n\t\tself._path = path\n\t\tself._match = None\n\t\t# TODO: _proxy is being used in settingsdevice.py, make a getter for that\n\t\tself._proxy = bus.get_object(serviceName, path, introspect=False)\n\t\tself.eventCallback = eventCallback\n\n\t\tassert eventCallback is None or createsignal == True\n\t\tif createsignal:\n\t\t\tself._match = self._proxy.connect_to_signal(\n\t\t\t\t\"PropertiesChanged\", weak_functor(self._properties_changed_handler))\n\t\t\tself._roots[serviceName].add(self)\n\n\t\t# store the current value in _cachedvalue. When it doesn't exists set _cachedvalue to\n\t\t# None, same as when a value is invalid\n\t\tself._cachedvalue = None\n\t\ttry:\n\t\t\tv = self._proxy.GetValue()\n\t\texcept dbus.exceptions.DBusException:\n\t\t\tpass\n\t\telse:\n\t\t\tself._cachedvalue = unwrap_dbus_value(v)\n\n\tdef __del__(self):\n\t\tif self._match is not None:\n\t\t\tself._match.remove()\n\t\t\tself._match = None\n\t\tself._proxy = None\n\n\tdef _refreshcachedvalue(self):\n\t\tself._cachedvalue = unwrap_dbus_value(self._proxy.GetValue())\n\n\t## Returns the path as a string, for example '/AC/L1/V'\n\t@property\n\tdef path(self):\n\t\treturn self._path\n\n\t## Returns the dbus service name as a string, for example com.victronenergy.vebus.ttyO1\n\t@property\n\tdef serviceName(self):\n\t\treturn self._serviceName\n\n\t## Returns the value of the dbus-item.\n\t# the type will be a dbus variant, for example dbus.Int32(0, variant_level=1)\n\t# this is not a property to keep the name consistant with the com.victronenergy.busitem interface\n\t# returns None when the property is invalid\n\tdef get_value(self):\n\t\treturn self._cachedvalue\n\n\t## Writes a new value to the dbus-item\n\tdef set_value(self, newvalue):\n\t\tr = self._proxy.SetValue(wrap_dbus_value(newvalue))\n\n\t\t# instead of just saving the value, go to the dbus and get it. So we have the right type etc.\n\t\tif r == 0:\n\t\t\tself._refreshcachedvalue()\n\n\t\treturn r\n\n\t## Resets the item to its default value\n\tdef set_default(self):\n\t\tself._proxy.SetDefault()\n\t\tself._refreshcachedvalue()\n\n\t## Returns the text representation of the value.\n\t# For example when the value is an enum/int GetText might return the string\n\t# belonging to that enum value. Another example, for a voltage, GetValue\n\t# would return a float, 12.0Volt, and GetText could return 12 VDC.\n\t#\n\t# Note that this depends on how the dbus-producer has implemented this.\n\tdef get_text(self):\n\t\treturn self._proxy.GetText()\n\n\t## Returns true of object path exists, and false if it doesn't\n\t@property\n\tdef exists(self):\n\t\t# TODO: do some real check instead of this crazy thing.\n\t\tr = False\n\t\ttry:\n\t\t\tr = self._proxy.GetValue()\n\t\t\tr = True\n\t\texcept dbus.exceptions.DBusException:\n\t\t\tpass\n\n\t\treturn r\n\n\t## callback for the trigger-event.\n\t# @param eventCallback the event-callback-function.\n\t@property\n\tdef eventCallback(self):\n\t\treturn self._eventCallback\n\n\t@eventCallback.setter\n\tdef eventCallback(self, eventCallback):\n\t\tself._eventCallback = eventCallback\n\n\t## Is called when the value of the imported bus-item changes.\n\t# Stores the new value in our local cache, and calls the eventCallback, if set.\n\tdef _properties_changed_handler(self, changes):\n\t\tif \"Value\" in changes:\n\t\t\tchanges['Value'] = unwrap_dbus_value(changes['Value'])\n\t\t\tself._cachedvalue = changes['Value']\n\t\t\tif self._eventCallback:\n\t\t\t\t# The reason behind this try/except is to prevent errors silently ending up the an error\n\t\t\t\t# handler in the dbus code.\n\t\t\t\ttry:\n\t\t\t\t\tself._eventCallback(self._serviceName, self._path, changes)\n\t\t\t\texcept:\n\t\t\t\t\ttraceback.print_exc()\n\t\t\t\t\tos._exit(1)  # sys.exit() is not used, since that also throws an exception\n\n\nclass VeDbusTreeExport(dbus.service.Object):\n\tdef __init__(self, bus, objectPath, service):\n\t\tdbus.service.Object.__init__(self, bus, objectPath)\n\t\tself._service = service\n\t\tlogging.debug(\"VeDbusTreeExport %s has been created\" % objectPath)\n\n\tdef __del__(self):\n\t\t# self._get_path() will raise an exception when retrieved after the call to .remove_from_connection,\n\t\t# so we need a copy.\n\t\tpath = self._get_path()\n\t\tif path is None:\n\t\t\treturn\n\t\tself.remove_from_connection()\n\t\tlogging.debug(\"VeDbusTreeExport %s has been removed\" % path)\n\n\tdef _get_path(self):\n\t\tif len(self._locations) == 0:\n\t\t\treturn None\n\t\treturn self._locations[0][1]\n\n\tdef _get_value_handler(self, path, get_text=False):\n\t\tlogging.debug(\"_get_value_handler called for %s\" % path)\n\t\tr = {}\n\t\tpx = path\n\t\tif not px.endswith('/'):\n\t\t\tpx += '/'\n\t\tfor p, item in self._service._dbusobjects.items():\n\t\t\tif p.startswith(px):\n\t\t\t\tv = item.GetText() if get_text else wrap_dbus_value(item.local_get_value())\n\t\t\t\tr[p[len(px):]] = v\n\t\tlogging.debug(r)\n\t\treturn r\n\n\t@dbus.service.method('com.victronenergy.BusItem', out_signature='v')\n\tdef GetValue(self):\n\t\tvalue = self._get_value_handler(self._get_path())\n\t\treturn dbus.Dictionary(value, signature=dbus.Signature('sv'), variant_level=1)\n\n\t@dbus.service.method('com.victronenergy.BusItem', out_signature='v')\n\tdef GetText(self):\n\t\treturn self._get_value_handler(self._get_path(), True)\n\n\tdef local_get_value(self):\n\t\treturn self._get_value_handler(self.path)\n\nclass VeDbusRootExport(VeDbusTreeExport):\n\t@dbus.service.signal('com.victronenergy.BusItem', signature='a{sa{sv}}')\n\tdef ItemsChanged(self, changes):\n\t\tpass\n\n\t@dbus.service.method('com.victronenergy.BusItem', out_signature='a{sa{sv}}')\n\tdef GetItems(self):\n\t\treturn {\n\t\t\tpath: {\n\t\t\t\t'Value': wrap_dbus_value(item.local_get_value()),\n\t\t\t\t'Text': item.GetText() }\n\t\t\tfor path, item in self._service._dbusobjects.items()\n\t\t}\n\n\nclass VeDbusItemExport(dbus.service.Object):\n\t## Constructor of VeDbusItemExport\n\t#\n\t# Use this object to export (publish), values on the dbus\n\t# Creates the dbus-object under the given dbus-service-name.\n\t# @param bus\t\t  The dbus object.\n\t# @param objectPath\t  The dbus-object-path.\n\t# @param value\t\t  Value to initialize ourselves with, defaults to None which means Invalid\n\t# @param description  String containing a description. Can be called over the dbus with GetDescription()\n\t# @param writeable\t  what would this do!? :).\n\t# @param callback\t  Function that will be called when someone else changes the value of this VeBusItem\n\t#                     over the dbus. First parameter passed to callback will be our path, second the new\n\t#\t\t\t\t\t  value. This callback should return True to accept the change, False to reject it.\n\tdef __init__(self, bus, objectPath, value=None, description=None, writeable=False,\n\t\t\t\t\tonchangecallback=None, gettextcallback=None, deletecallback=None,\n\t\t\t\t\tvaluetype=None):\n\t\tdbus.service.Object.__init__(self, bus, objectPath)\n\t\tself._onchangecallback = onchangecallback\n\t\tself._gettextcallback = gettextcallback\n\t\tself._value = value\n\t\tself._description = description\n\t\tself._writeable = writeable\n\t\tself._deletecallback = deletecallback\n\t\tself._type = valuetype\n\n\t# To force immediate deregistering of this dbus object, explicitly call __del__().\n\tdef __del__(self):\n\t\t# self._get_path() will raise an exception when retrieved after the\n\t\t# call to .remove_from_connection, so we need a copy.\n\t\tpath = self._get_path()\n\t\tif path == None:\n\t\t\treturn\n\t\tif self._deletecallback is not None:\n\t\t\tself._deletecallback(path)\n\t\tself.remove_from_connection()\n\t\tlogging.debug(\"VeDbusItemExport %s has been removed\" % path)\n\n\tdef _get_path(self):\n\t\tif len(self._locations) == 0:\n\t\t\treturn None\n\t\treturn self._locations[0][1]\n\n\t## Sets the value. And in case the value is different from what it was, a signal\n\t# will be emitted to the dbus. This function is to be used in the python code that\n\t# is using this class to export values to the dbus.\n\t# set value to None to indicate that it is Invalid\n\tdef local_set_value(self, newvalue):\n\t\tchanges = self._local_set_value(newvalue)\n\t\tif changes is not None:\n\t\t\tself.PropertiesChanged(changes)\n\n\tdef _local_set_value(self, newvalue):\n\t\tif self._value == newvalue:\n\t\t\treturn None\n\n\t\tself._value = newvalue\n\t\treturn {\n\t\t\t'Value': wrap_dbus_value(newvalue),\n\t\t\t'Text': self.GetText()\n\t\t}\n\n\tdef local_get_value(self):\n\t\treturn self._value\n\n\t# ==== ALL FUNCTIONS BELOW THIS LINE WILL BE CALLED BY OTHER PROCESSES OVER THE DBUS ====\n\n\t## Dbus exported method SetValue\n\t# Function is called over the D-Bus by other process. It will first check (via callback) if new\n\t# value is accepted. And it is, stores it and emits a changed-signal.\n\t# @param value The new value.\n\t# @return completion-code When successful a 0 is return, and when not a -1 is returned.\n\t@dbus.service.method('com.victronenergy.BusItem', in_signature='v', out_signature='i')\n\tdef SetValue(self, newvalue):\n\t\tif not self._writeable:\n\t\t\treturn 1  # NOT OK\n\n\t\tnewvalue = unwrap_dbus_value(newvalue)\n\n\t\t# If value type is enforced, cast it. If the type can be coerced\n\t\t# python will do it for us. This allows ints to become floats,\n\t\t# or bools to become ints. Additionally also allow None, so that\n\t\t# a path may be invalidated.\n\t\tif self._type is not None and newvalue is not None:\n\t\t\ttry:\n\t\t\t\tnewvalue = self._type(newvalue)\n\t\t\texcept (ValueError, TypeError):\n\t\t\t\treturn 1 # NOT OK\n\n\t\tif newvalue == self._value:\n\t\t\treturn 0  # OK\n\n\t\t# call the callback given to us, and check if new value is OK.\n\t\tif (self._onchangecallback is None or\n\t\t\t\t(self._onchangecallback is not None and self._onchangecallback(self.__dbus_object_path__, newvalue))):\n\n\t\t\tself.local_set_value(newvalue)\n\t\t\treturn 0  # OK\n\n\t\treturn 2  # NOT OK\n\n\t## Dbus exported method GetDescription\n\t#\n\t# Returns the a description.\n\t# @param language A language code (e.g. ISO 639-1 en-US).\n\t# @param length Lenght of the language string.\n\t# @return description\n\t@dbus.service.method('com.victronenergy.BusItem', in_signature='si', out_signature='s')\n\tdef GetDescription(self, language, length):\n\t\treturn self._description if self._description is not None else 'No description given'\n\n\t## Dbus exported method GetValue\n\t# Returns the value.\n\t# @return the value when valid, and otherwise an empty array\n\t@dbus.service.method('com.victronenergy.BusItem', out_signature='v')\n\tdef GetValue(self):\n\t\treturn wrap_dbus_value(self._value)\n\n\t## Dbus exported method GetText\n\t# Returns the value as string of the dbus-object-path.\n\t# @return text A text-value. '---' when local value is invalid\n\t@dbus.service.method('com.victronenergy.BusItem', out_signature='s')\n\tdef GetText(self):\n\t\tif self._value is None:\n\t\t\treturn '---'\n\n\t\t# Default conversion from dbus.Byte will get you a character (so 'T' instead of '84'), so we\n\t\t# have to convert to int first. Note that if a dbus.Byte turns up here, it must have come from\n\t\t# the application itself, as all data from the D-Bus should have been unwrapped by now.\n\t\tif self._gettextcallback is None and type(self._value) == dbus.Byte:\n\t\t\treturn str(int(self._value))\n\n\t\tif self._gettextcallback is None and self.__dbus_object_path__ == '/ProductId':\n\t\t\treturn \"0x%X\" % self._value\n\n\t\tif self._gettextcallback is None:\n\t\t\treturn str(self._value)\n\n\t\treturn self._gettextcallback(self.__dbus_object_path__, self._value)\n\n\t## The signal that indicates that the value has changed.\n\t# Other processes connected to this BusItem object will have subscribed to the\n\t# event when they want to track our state.\n\t@dbus.service.signal('com.victronenergy.BusItem', signature='a{sv}')\n\tdef PropertiesChanged(self, changes):\n\t\tpass\n\n## This class behaves like a regular reference to a class method (eg. self.foo), but keeps a weak reference\n## to the object which method is to be called.\n## Use this object to break circular references.\nclass weak_functor:\n\tdef __init__(self, f):\n\t\tself._r = weakref.ref(f.__self__)\n\t\tself._f = weakref.ref(f.__func__)\n\n\tdef __call__(self, *args, **kargs):\n\t\tr = self._r()\n\t\tf = self._f()\n\t\tif r == None or f == None:\n\t\t\treturn\n\t\tf(r, *args, **kargs)\n"
  },
  {
    "path": "velib_python/velib_python/latest/dbusmonitor.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\n## @package dbus_vrm\n# This code takes care of the D-Bus interface (not all of below is implemented yet):\n# - on startup it scans the dbus for services we know. For each known service found, it searches for\n#   objects/paths we know. Everything we find is stored in items{}, and an event is registered: if a\n#   value changes weĺl be notified and can pass that on to our owner. For example the vrmLogger.\n#   we know.\n# - after startup, it continues to monitor the dbus:\n#\t\t1) when services are added we do the same check on that\n#\t\t2) when services are removed, we remove any items that we had that referred to that service\n#\t\t3) if an existing services adds paths we update ourselves as well: on init, we make a\n#\t\t   VeDbusItemImport for a non-, or not yet existing objectpaths as well1\n#\n# Code is used by the vrmLogger, and also the pubsub code. Both are other modules in the dbus_vrm repo.\n\nfrom dbus.mainloop.glib import DBusGMainLoop\nfrom gi.repository import GLib\nimport dbus\nimport dbus.service\nimport inspect\nimport logging\nimport argparse\nimport pprint\nimport traceback\nimport os\nfrom collections import defaultdict\nfrom functools import partial\n\n# our own packages\nfrom ve_utils import exit_on_error, wrap_dbus_value, unwrap_dbus_value, add_name_owner_changed_receiver\n\n# dbus interface\nVE_INTERFACE = \"com.victronenergy.BusItem\"\n\n# For lookups where None is a valid result\nnotfound = object()\n\nlogger = logging.getLogger(__name__)\nlogger.setLevel(logging.INFO)\nclass SystemBus(dbus.bus.BusConnection):\n\tdef __new__(cls):\n\t\treturn dbus.bus.BusConnection.__new__(cls, dbus.bus.BusConnection.TYPE_SYSTEM)\n\nclass SessionBus(dbus.bus.BusConnection):\n\tdef __new__(cls):\n\t\treturn dbus.bus.BusConnection.__new__(cls, dbus.bus.BusConnection.TYPE_SESSION)\n\nclass MonitoredValue(object):\n\tdef __init__(self, value, text, options):\n\t\tsuper(MonitoredValue, self).__init__()\n\t\tself.value = value\n\t\tself.text = text\n\t\tself.options = options\n\n\t# For legacy code, allow treating this as a tuple/list\n\tdef __iter__(self):\n\t\treturn iter((self.value, self.text, self.options))\n\nclass Service(object):\n\tdef __init__(self, id, serviceName, deviceInstance):\n\t\tsuper(Service, self).__init__()\n\t\tself.id = id\n\t\tself.name = serviceName\n\t\tself.paths = {}\n\t\tself._seen = set()\n\t\tself.deviceInstance = deviceInstance\n\n\t# For legacy code, attributes can still be accessed as if keys from a\n\t# dictionary.\n\tdef __setitem__(self, key, value):\n\t\tself.__dict__[key] = value\n\tdef __getitem__(self, key):\n\t\treturn self.__dict__[key]\n\n\tdef set_seen(self, path):\n\t\tself._seen.add(path)\n\n\tdef seen(self, path):\n\t\treturn path in self._seen\n\n\t@property\n\tdef service_class(self):\n\t\treturn '.'.join(self.name.split('.')[:3])\n\nclass DbusMonitor(object):\n\t## Constructor\n\tdef __init__(self, dbusTree, valueChangedCallback=None, deviceAddedCallback=None,\n\t\t\t\t\tdeviceRemovedCallback=None, namespace=\"com.victronenergy\", ignoreServices=[]):\n\t\t# valueChangedCallback is the callback that we call when something has changed.\n\t\t# def value_changed_on_dbus(dbusServiceName, dbusPath, options, changes, deviceInstance):\n\t\t# in which changes is a tuple with GetText() and GetValue()\n\t\tself.valueChangedCallback = valueChangedCallback\n\t\tself.deviceAddedCallback = deviceAddedCallback\n\t\tself.deviceRemovedCallback = deviceRemovedCallback\n\t\tself.dbusTree = dbusTree\n\t\tself.ignoreServices = ignoreServices\n\n\t\t# Lists all tracked services. Stores name, id, device instance, value per path, and whenToLog info\n\t\t# indexed by service name (eg. com.victronenergy.settings).\n\t\tself.servicesByName = {}\n\n\t\t# Same values as self.servicesByName, but indexed by service id (eg. :1.30)\n\t\tself.servicesById = {}\n\n\t\t# Keep track of services by class to speed up calls to get_service_list\n\t\tself.servicesByClass = defaultdict(list)\n\n\t\t# Keep track of any additional watches placed on items\n\t\tself.serviceWatches = defaultdict(list)\n\n\t\t# For a PC, connect to the SessionBus\n\t\t# For a CCGX, connect to the SystemBus\n\t\tself.dbusConn = SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else SystemBus()\n\n\t\t# subscribe to NameOwnerChange for bus connect / disconnect events.\n\t\t# NOTE: this is on a different bus then the one above!\n\t\tstandardBus = (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ \\\n\t\t\telse dbus.SystemBus())\n\n\t\tadd_name_owner_changed_receiver(standardBus, self.dbus_name_owner_changed)\n\n\t\t# Subscribe to PropertiesChanged for all services\n\t\tself.dbusConn.add_signal_receiver(self.handler_value_changes,\n\t\t\tdbus_interface='com.victronenergy.BusItem',\n\t\t\tsignal_name='PropertiesChanged', path_keyword='path',\n\t\t\tsender_keyword='senderId')\n\n\t\t# Subscribe to ItemsChanged for all services\n\t\tself.dbusConn.add_signal_receiver(self.handler_item_changes,\n\t\t\tdbus_interface='com.victronenergy.BusItem',\n\t\t\tsignal_name='ItemsChanged', path='/',\n\t\t\tsender_keyword='senderId')\n\n\t\tlogger.info('===== Search on dbus for services that we will monitor starting... =====')\n\t\tserviceNames = self.dbusConn.list_names()\n\t\tfor serviceName in serviceNames:\n\t\t\tself.scan_dbus_service(serviceName)\n\n\t\tlogger.info('===== Search on dbus for services that we will monitor finished =====')\n\n\t@staticmethod\n\tdef make_service(serviceId, serviceName, deviceInstance):\n\t\t\"\"\" Override this to use a different kind of service object. \"\"\"\n\t\treturn Service(serviceId, serviceName, deviceInstance)\n\n\tdef make_monitor(self, service, path, value, text, options):\n\t\t\"\"\" Override this to do more things with monitoring. \"\"\"\n\t\treturn MonitoredValue(unwrap_dbus_value(value), unwrap_dbus_value(text), options)\n\n\tdef dbus_name_owner_changed(self, name, oldowner, newowner):\n\t\tif not name.startswith(\"com.victronenergy.\"):\n\t\t\treturn\n\n\t\t#decouple, and process in main loop\n\t\tGLib.idle_add(exit_on_error, self._process_name_owner_changed, name, oldowner, newowner)\n\n\tdef _process_name_owner_changed(self, name, oldowner, newowner):\n\t\tif newowner != '':\n\t\t\t# so we found some new service. Check if we can do something with it.\n\t\t\tnewdeviceadded = self.scan_dbus_service(name)\n\t\t\tif newdeviceadded and self.deviceAddedCallback is not None:\n\t\t\t\tself.deviceAddedCallback(name, self.get_device_instance(name))\n\n\t\telif name in self.servicesByName:\n\t\t\t# it disappeared, we need to remove it.\n\t\t\tlogger.info(\"%s disappeared from the dbus. Removing it from our lists\" % name)\n\t\t\tservice = self.servicesByName[name]\n\t\t\tdel self.servicesById[service.id]\n\t\t\tdel self.servicesByName[name]\n\t\t\tfor watch in self.serviceWatches[name]:\n\t\t\t\twatch.remove()\n\t\t\tdel self.serviceWatches[name]\n\t\t\tself.servicesByClass[service.service_class].remove(service)\n\t\t\tif self.deviceRemovedCallback is not None:\n\t\t\t\tself.deviceRemovedCallback(name, service.deviceInstance)\n\n\tdef scan_dbus_service(self, serviceName):\n\t\ttry:\n\t\t\treturn self.scan_dbus_service_inner(serviceName)\n\t\texcept:\n\t\t\tlogger.error(\"Ignoring %s because of error while scanning:\" % (serviceName))\n\t\t\ttraceback.print_exc()\n\t\t\treturn False\n\n\t\t\t# Errors 'org.freedesktop.DBus.Error.ServiceUnknown' and\n\t\t\t# 'org.freedesktop.DBus.Error.Disconnected' seem to happen when the service\n\t\t\t# disappears while its being scanned. Which might happen, but is not really\n\t\t\t# normal either, so letting them go into the logs.\n\n\t# Scans the given dbus service to see if it contains anything interesting for us. If it does, add\n\t# it to our list of monitored D-Bus services.\n\tdef scan_dbus_service_inner(self, serviceName):\n\n\t\t# make it a normal string instead of dbus string\n\t\tserviceName = str(serviceName)\n\n\t\tif (len(self.ignoreServices) != 0 and any(serviceName.startswith(x) for x in self.ignoreServices)):\n\t\t\tlogger.debug(\"Ignoring service %s\" % serviceName)\n\t\t\treturn False\n\n\t\tpaths = self.dbusTree.get('.'.join(serviceName.split('.')[0:3]), None)\n\t\tif paths is None:\n\t\t\tlogger.debug(\"Ignoring service %s, not in the tree\" % serviceName)\n\t\t\treturn False\n\n\t\tlogger.info(\"Found: %s, scanning and storing items\" % serviceName)\n\t\tserviceId = self.dbusConn.get_name_owner(serviceName)\n\n\t\t# we should never be notified to add a D-Bus service that we already have. If this assertion\n\t\t# raises, check process_name_owner_changed, and D-Bus workings.\n\t\tassert serviceName not in self.servicesByName\n\t\tassert serviceId not in self.servicesById\n\n\t\t# Try to fetch everything with a GetItems, then fall back to older\n\t\t# methods if that fails\n\t\ttry:\n\t\t\tvalues = self.dbusConn.call_blocking(serviceName, '/', VE_INTERFACE, 'GetItems', '', [])\n\t\texcept dbus.exceptions.DBusException:\n\t\t\tlogger.info(\"GetItems failed, trying legacy methods\")\n\t\telse:\n\t\t\treturn self.scan_dbus_service_getitems_done(serviceName, serviceId, values)\n\n\t\tif serviceName == 'com.victronenergy.settings':\n\t\t\tdi = 0\n\t\telif serviceName.startswith('com.victronenergy.vecan.'):\n\t\t\tdi = 0\n\t\telse:\n\t\t\ttry:\n\t\t\t\tdi = self.dbusConn.call_blocking(serviceName,\n\t\t\t\t\t'/DeviceInstance', VE_INTERFACE, 'GetValue', '', [])\n\t\t\texcept dbus.exceptions.DBusException:\n\t\t\t\tlogger.info(\"       %s was skipped because it has no device instance\" % serviceName)\n\t\t\t\treturn False # Skip it\n\t\t\telse:\n\t\t\t\tdi = int(di)\n\n\t\tlogger.info(\"       %s has device instance %s\" % (serviceName, di))\n\t\tservice = self.make_service(serviceId, serviceName, di)\n\n\t\t# Let's try to fetch everything in one go\n\t\tvalues = {}\n\t\ttexts = {}\n\t\ttry:\n\t\t\tvalues.update(self.dbusConn.call_blocking(serviceName, '/', VE_INTERFACE, 'GetValue', '', []))\n\t\t\ttexts.update(self.dbusConn.call_blocking(serviceName, '/', VE_INTERFACE, 'GetText', '', []))\n\t\texcept:\n\t\t\tpass\n\n\t\tfor path, options in paths.items():\n\t\t\t# path will be the D-Bus path: '/Ac/ActiveIn/L1/V'\n\t\t\t# options will be a dictionary: {'code': 'V', 'whenToLog': 'onIntervalAlways'}\n\n\t\t\t# Try to obtain the value we want from our bulk fetch. If we\n\t\t\t# cannot find it there, do an individual query.\n\t\t\tvalue = values.get(path[1:], notfound)\n\t\t\tif value != notfound:\n\t\t\t\tservice.set_seen(path)\n\t\t\ttext = texts.get(path[1:], notfound)\n\t\t\tif value is notfound or text is notfound:\n\t\t\t\ttry:\n\t\t\t\t\tvalue = self.dbusConn.call_blocking(serviceName, path, VE_INTERFACE, 'GetValue', '', [])\n\t\t\t\t\tservice.set_seen(path)\n\t\t\t\t\ttext = self.dbusConn.call_blocking(serviceName, path, VE_INTERFACE, 'GetText', '', [])\n\t\t\t\texcept dbus.exceptions.DBusException as e:\n\t\t\t\t\tif e.get_dbus_name() in (\n\t\t\t\t\t\t\t'org.freedesktop.DBus.Error.ServiceUnknown',\n\t\t\t\t\t\t\t'org.freedesktop.DBus.Error.Disconnected'):\n\t\t\t\t\t\traise # This exception will be handled below\n\n\t\t\t\t\t# TODO org.freedesktop.DBus.Error.UnknownMethod really\n\t\t\t\t\t# shouldn't happen but sometimes does.\n\t\t\t\t\tlogger.debug(\"%s %s does not exist (yet)\" % (serviceName, path))\n\t\t\t\t\tvalue = None\n\t\t\t\t\ttext = None\n\n\t\t\tservice.paths[path] = self.make_monitor(service, path, unwrap_dbus_value(value), unwrap_dbus_value(text), options)\n\n\n\t\tlogger.debug(\"Finished scanning and storing items for %s\" % serviceName)\n\n\t\t# Adjust self at the end of the scan, so we don't have an incomplete set of\n\t\t# data if an exception occurs during the scan.\n\t\tself.servicesByName[serviceName] = service\n\t\tself.servicesById[serviceId] = service\n\t\tself.servicesByClass[service.service_class].append(service)\n\n\t\treturn True\n\n\tdef scan_dbus_service_getitems_done(self, serviceName, serviceId, values):\n\t\t# Keeping these exceptions for legacy reasons\n\t\tif serviceName == 'com.victronenergy.settings':\n\t\t\tdi = 0\n\t\telif serviceName.startswith('com.victronenergy.vecan.'):\n\t\t\tdi = 0\n\t\telse:\n\t\t\ttry:\n\t\t\t\tdi = values['/DeviceInstance']['Value']\n\t\t\texcept KeyError:\n\t\t\t\tlogger.info(\"       %s was skipped because it has no device instance\" % serviceName)\n\t\t\t\treturn False\n\t\t\telse:\n\t\t\t\tdi = int(di)\n\n\t\tlogger.info(\"       %s has device instance %s\" % (serviceName, di))\n\t\tservice = self.make_service(serviceId, serviceName, di)\n\n\t\tpaths = self.dbusTree.get('.'.join(serviceName.split('.')[0:3]), {})\n\t\tfor path, options in paths.items():\n\t\t\titem = values.get(path, notfound)\n\t\t\tif item is notfound:\n\t\t\t\tservice.paths[path] = self.make_monitor(service, path, None, None, options)\n\t\t\telse:\n\t\t\t\tservice.set_seen(path)\n\t\t\t\tvalue = item.get('Value', None)\n\t\t\t\ttext = item.get('Text', None)\n\t\t\t\tservice.paths[path] = self.make_monitor(service, path, unwrap_dbus_value(value), unwrap_dbus_value(text), options)\n\n\t\tself.servicesByName[serviceName] = service\n\t\tself.servicesById[serviceId] = service\n\t\tself.servicesByClass[service.service_class].append(service)\n\t\treturn True\n\n\tdef handler_item_changes(self, items, senderId):\n\t\tif not isinstance(items, dict):\n\t\t\treturn\n\n\t\ttry:\n\t\t\tservice = self.servicesById[senderId]\n\t\texcept KeyError:\n\t\t\t# senderId isn't there, which means it hasn't been scanned yet.\n\t\t\treturn\n\n\t\tfor path, changes in items.items():\n\t\t\ttry:\n\t\t\t\tv = unwrap_dbus_value(changes['Value'])\n\t\t\texcept (KeyError, TypeError):\n\t\t\t\tcontinue\n\n\t\t\ttry:\n\t\t\t\tt = changes['Text']\n\t\t\texcept KeyError:\n\t\t\t\tt = str(v)\n\t\t\tself._handler_value_changes(service, path, v, t)\n\n\tdef handler_value_changes(self, changes, path, senderId):\n\t\t# If this properyChange does not involve a value, our work is done.\n\t\tif 'Value' not in changes:\n\t\t\treturn\n\n\t\ttry:\n\t\t\tservice = self.servicesById[senderId]\n\t\texcept KeyError:\n\t\t\t# senderId isn't there, which means it hasn't been scanned yet.\n\t\t\treturn\n\n\t\tv = unwrap_dbus_value(changes['Value'])\n\t\t# Some services don't send Text with their PropertiesChanged events.\n\t\ttry:\n\t\t\tt = changes['Text']\n\t\texcept KeyError:\n\t\t\tt = str(v)\n\t\tself._handler_value_changes(service, path, v, t)\n\n\tdef _handler_value_changes(self, service, path, value, text):\n\t\ttry:\n\t\t\ta = service.paths[path]\n\t\texcept KeyError:\n\t\t\t# path isn't there, which means it hasn't been scanned yet.\n\t\t\treturn\n\n\t\tservice.set_seen(path)\n\n\t\t# First update our store to the new value\n\t\tif a.value == value:\n\t\t\treturn\n\n\t\ta.value = value\n\t\ta.text = text\n\n\t\t# And do the rest of the processing in on the mainloop\n\t\tif self.valueChangedCallback is not None:\n\t\t\tGLib.idle_add(exit_on_error, self._execute_value_changes, service.name, path, {\n\t\t\t\t'Value': value, 'Text': text}, a.options)\n\n\tdef _execute_value_changes(self, serviceName, objectPath, changes, options):\n\t\t# double check that the service still exists, as it might have\n\t\t# disappeared between scheduling-for and executing this function.\n\t\tif serviceName not in self.servicesByName:\n\t\t\treturn\n\n\t\tself.valueChangedCallback(serviceName, objectPath,\n\t\t\toptions, changes, self.get_device_instance(serviceName))\n\n\t# Gets the value for a certain servicename and path\n\t# The default_value is returned when:\n\t# 1. When the service doesn't exist.\n\t# 2. When the path asked for isn't being monitored.\n\t# 3. When the path exists, but has dbus-invalid, ie an empty byte array.\n\t# 4. When the path asked for is being monitored, but doesn't exist for that service.\n\tdef get_value(self, serviceName, objectPath, default_value=None):\n\t\tservice = self.servicesByName.get(serviceName, None)\n\t\tif service is None:\n\t\t\treturn default_value\n\n\t\tvalue = service.paths.get(objectPath, None)\n\t\tif value is None or value.value is None:\n\t\t\treturn default_value\n\n\t\treturn value.value\n\n\t# returns if a dbus exists now, by doing a blocking dbus call.\n\t# Typically seen will be sufficient and doesn't need access to the dbus.\n\tdef exists(self, serviceName, objectPath):\n\t\ttry:\n\t\t\tself.dbusConn.call_blocking(serviceName, objectPath, VE_INTERFACE, 'GetValue', '', [])\n\t\t\treturn True\n\t\texcept dbus.exceptions.DBusException as e:\n\t\t\treturn False\n\n\t# Returns if there ever was a successful GetValue or valueChanged event.\n\t# Unlike get_value this return True also if the actual value is invalid.\n\t#\n\t# Note: the path might no longer exists anymore, but that doesn't happen in\n\t# practice. If a service really wants to reconfigure itself typically it should\n\t# reconnect to the dbus which causes it to be rescanned and seen will be updated.\n\t# If it is really needed to know if a path still exists, use exists.\n\tdef seen(self, serviceName, objectPath):\n\t\ttry:\n\t\t\treturn self.servicesByName[serviceName].seen(objectPath)\n\t\texcept KeyError:\n\t\t\treturn False\n\n\t# Sets the value for a certain servicename and path, returns the return value of the D-Bus SetValue\n\t# method. If the underlying item does not exist (the service does not exist, or the objectPath was not\n\t# registered) the function will return -1\n\tdef set_value(self, serviceName, objectPath, value):\n\t\t# Check if the D-Bus object referenced by serviceName and objectPath is registered. There is no\n\t\t# necessity to do this, but it is in line with previous implementations which kept VeDbusItemImport\n\t\t# objects for registers items only.\n\t\tservice = self.servicesByName.get(serviceName, None)\n\t\tif service is None:\n\t\t\treturn -1\n\t\tif objectPath not in service.paths:\n\t\t\treturn -1\n\t\t# We do not catch D-Bus exceptions here, because the previous implementation did not do that either.\n\t\treturn self.dbusConn.call_blocking(serviceName, objectPath,\n\t\t\t\t   dbus_interface=VE_INTERFACE,\n\t\t\t\t   method='SetValue', signature=None,\n\t\t\t\t   args=[wrap_dbus_value(value)])\n\n\t# Similar to set_value, but operates asynchronously\n\tdef set_value_async(self, serviceName, objectPath, value,\n\t\t\treply_handler=None, error_handler=None):\n\t\tservice = self.servicesByName.get(serviceName, None)\n\t\tif service is not None:\n\t\t\tif objectPath in service.paths:\n\t\t\t\tself.dbusConn.call_async(serviceName, objectPath,\n\t\t\t\t\tdbus_interface=VE_INTERFACE,\n\t\t\t\t\tmethod='SetValue', signature=None,\n\t\t\t\t\targs=[wrap_dbus_value(value)],\n\t\t\t\t\treply_handler=reply_handler, error_handler=error_handler)\n\t\t\t\treturn\n\n\t\tif error_handler is not None:\n\t\t\terror_handler(TypeError('Service or path not found, '\n\t\t\t\t\t\t'service=%s, path=%s' % (serviceName, objectPath)))\n\n\t# returns a dictionary, keys are the servicenames, value the instances\n\t# optionally use the classfilter to get only a certain type of services, for\n\t# example com.victronenergy.battery.\n\tdef get_service_list(self, classfilter=None):\n\t\tif classfilter is None:\n\t\t\treturn { servicename: service.deviceInstance \\\n\t\t\t\tfor servicename, service in self.servicesByName.items() }\n\n\t\tif classfilter not in self.servicesByClass:\n\t\t\treturn {}\n\n\t\treturn { service.name: service.deviceInstance \\\n\t\t\tfor service in self.servicesByClass[classfilter] }\n\n\tdef get_device_instance(self, serviceName):\n\t\treturn self.servicesByName[serviceName].deviceInstance\n\n\tdef track_value(self, serviceName, objectPath, callback, *args, **kwargs):\n\t\t\"\"\" A DbusMonitor can watch specific service/path combos for changes\n\t\t    so that it is not fully reliant on the global handler_value_changes\n\t\t    in this class. Additional watches are deleted automatically when\n\t\t    the service disappears from dbus. \"\"\"\n\t\tcb = partial(callback, *args, **kwargs)\n\n\t\tdef root_tracker(items):\n\t\t\t# Check if objectPath in dict\n\t\t\ttry:\n\t\t\t\tv = items[objectPath]\n\t\t\t\t_v = unwrap_dbus_value(v['Value'])\n\t\t\texcept (KeyError, TypeError):\n\t\t\t\treturn # not in this dict\n\n\t\t\ttry:\n\t\t\t\tt = v['Text']\n\t\t\texcept KeyError:\n\t\t\t\tcb({'Value': _v })\n\t\t\telse:\n\t\t\t\tcb({'Value': _v, 'Text': t})\n\n\t\t# Track changes on the path, and also on root\n\t\tself.serviceWatches[serviceName].extend((\n\t\t\tself.dbusConn.add_signal_receiver(cb,\n\t\t\t\tdbus_interface='com.victronenergy.BusItem',\n\t\t\t\tsignal_name='PropertiesChanged',\n\t\t\t\tpath=objectPath, bus_name=serviceName),\n\t\t\tself.dbusConn.add_signal_receiver(root_tracker,\n\t\t\t\tdbus_interface='com.victronenergy.BusItem',\n\t\t\t\tsignal_name='ItemsChanged',\n\t\t\t\tpath=\"/\", bus_name=serviceName),\n\t\t))\n\n\n# ====== ALL CODE BELOW THIS LINE IS PURELY FOR DEVELOPING THIS CLASS ======\n\n# Example function that can be used as a starting point to use this code\ndef value_changed_on_dbus(dbusServiceName, dbusPath, dict, changes, deviceInstance):\n\tlogger.debug(\"0 ----------------\")\n\tlogger.debug(\"1 %s%s changed\" % (dbusServiceName, dbusPath))\n\tlogger.debug(\"2 vrm dict     : %s\" % dict)\n\tlogger.debug(\"3 changes-text: %s\" % changes['Text'])\n\tlogger.debug(\"4 changes-value: %s\" % changes['Value'])\n\tlogger.debug(\"5 deviceInstance: %s\" % deviceInstance)\n\tlogger.debug(\"6 - end\")\n\n\ndef nameownerchange(a, b):\n\t# used to find memory leaks in dbusmonitor and VeDbusItemImport\n\timport gc\n\tgc.collect()\n\tobjects = gc.get_objects()\n\tprint (len([o for o in objects if type(o).__name__ == 'VeDbusItemImport']))\n\tprint (len([o for o in objects if type(o).__name__ == 'SignalMatch']))\n\tprint (len(objects))\n\n\ndef print_values(dbusmonitor):\n\ta = dbusmonitor.get_value('wrongservice', '/DbusInvalid', default_value=1000)\n\tb = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/NotInTheMonitorList', default_value=1000)\n\tc = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/DbusInvalid', default_value=1000)\n\td = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/NonExistingButMonitored', default_value=1000)\n\n\tprint (\"All should be 1000: Wrong Service: %s, NotInTheMonitorList: %s, DbusInvalid: %s, NonExistingButMonitored: %s\" % (a, b, c, d))\n\treturn True\n\n# We have a mainloop, but that is just for developing this code. Normally above class & code is used from\n# some other class, such as vrmLogger or the pubsub Implementation.\ndef main():\n\t# Init logging\n\tlogging.basicConfig(level=logging.DEBUG)\n\tlogger.info(__file__ + \" is starting up\")\n\n\t# Have a mainloop, so we can send/receive asynchronous calls to and from dbus\n\tDBusGMainLoop(set_as_default=True)\n\n\timport os\n\timport sys\n\tsys.path.insert(1, os.path.join(os.path.dirname(__file__), '../../'))\n\n\tdummy = {'code': None, 'whenToLog': 'configChange', 'accessLevel': None}\n\tmonitorlist = {'com.victronenergy.dummyservice': {\n\t\t\t\t'/Connected': dummy,\n\t\t\t\t'/ProductName': dummy,\n\t\t\t\t'/Mgmt/Connection': dummy,\n\t\t\t\t'/Dc/0/Voltage': dummy,\n\t\t\t\t'/Dc/0/Current': dummy,\n\t\t\t\t'/Dc/0/Temperature': dummy,\n\t\t\t\t'/Load/I': dummy,\n\t\t\t\t'/FirmwareVersion': dummy,\n\t\t\t\t'/DbusInvalid': dummy,\n\t\t\t\t'/NonExistingButMonitored': dummy}}\n\n\td = DbusMonitor(monitorlist, value_changed_on_dbus,\n\t\tdeviceAddedCallback=nameownerchange, deviceRemovedCallback=nameownerchange)\n\n\tGLib.timeout_add(1000, print_values, d)\n\n\t# Start and run the mainloop\n\tlogger.info(\"Starting mainloop, responding on only events\")\n\tmainloop = GLib.MainLoop()\n\tmainloop.run()\n\nif __name__ == \"__main__\":\n\tmain()\n"
  },
  {
    "path": "velib_python/velib_python/latest/oldestVersion",
    "content": "v3.50\n"
  },
  {
    "path": "velib_python/velib_python/latest/settingsdevice.py",
    "content": "import dbus\nimport logging\nimport time\nfrom functools import partial\n\n# Local imports\nfrom vedbus import VeDbusItemImport\n\n## Indexes for the setting dictonary.\nPATH = 0\nVALUE = 1\nMINIMUM = 2\nMAXIMUM = 3\nSILENT = 4\n\n## The Settings Device class.\n# Used by python programs, such as the vrm-logger, to read and write settings they\n# need to store on disk. And since these settings might be changed from a different\n# source, such as the GUI, the program can pass an eventCallback that will be called\n# as soon as some setting is changed.\n#\n# The settings are stored in flash via the com.victronenergy.settings service on dbus.\n# See https://github.com/victronenergy/localsettings for more info.\n#\n# If there are settings in de supportSettings list which are not yet on the dbus, \n# and therefore not yet in the xml file, they will be added through the dbus-addSetting\n# interface of com.victronenergy.settings.\nclass SettingsDevice(object):\n\t## The constructor processes the tree of dbus-items.\n\t# @param bus the system-dbus object\n\t# @param name the dbus-service-name of the settings dbus service, 'com.victronenergy.settings'\n\t# @param supportedSettings dictionary with all setting-names, and their defaultvalue, min, max and whether\n\t# the setting is silent. The 'silent' entry is optional. If set to true, no changes in the setting will\n\t# be logged by localsettings.\n\t# @param eventCallback function that will be called on changes on any of these settings\n\t# @param timeout Maximum interval to wait for localsettings. An exception is thrown at the end of the\n\t# interval if the localsettings D-Bus service has not appeared yet.\n\tdef __init__(self, bus, supportedSettings, eventCallback, name='com.victronenergy.settings', timeout=0):\n\t\tlogging.debug(\"===== Settings device init starting... =====\")\n\t\tself._bus = bus\n\t\tself._dbus_name = name\n\t\tself._eventCallback = eventCallback\n\t\tself._values = {} # stored the values, used to pass the old value along on a setting change\n\t\tself._settings = {}\n\n\t\tcount = 0\n\t\twhile True:\n\t\t\tif 'com.victronenergy.settings' in self._bus.list_names():\n\t\t\t\tbreak\n\t\t\tif count == timeout:\n\t\t\t\traise Exception(\"The settings service com.victronenergy.settings does not exist!\")\n\t\t\tcount += 1\n\t\t\tlogging.info('waiting for settings')\n\t\t\ttime.sleep(1)\n\n\t\t# Add the items.\n\t\tself.addSettings(supportedSettings)\n\n\t\tlogging.debug(\"===== Settings device init finished =====\")\n\n\tdef addSettings(self, settings):\n\t\tfor setting, options in settings.items():\n\t\t\tsilent = len(options) > SILENT and options[SILENT]\n\t\t\tbusitem = self.addSetting(options[PATH], options[VALUE],\n\t\t\t\toptions[MINIMUM], options[MAXIMUM], silent, callback=partial(self.handleChangedSetting, setting))\n\t\t\tself._settings[setting] = busitem\n\t\t\tself._values[setting] = busitem.get_value()\n\n\tdef addSetting(self, path, value, _min, _max, silent=False, callback=None):\n\t\tbusitem = VeDbusItemImport(self._bus, self._dbus_name, path, callback)\n\t\tif busitem.exists and (value, _min, _max, silent) == busitem._proxy.GetAttributes():\n\t\t\tlogging.debug(\"Setting %s found\" % path)\n\t\telse:\n\t\t\tlogging.info(\"Setting %s does not exist yet or must be adjusted\" % path)\n\n\t\t\t# Prepare to add the setting. Most dbus types extend the python\n\t\t\t# type so it is only necessary to additionally test for Int64.\n\t\t\tif isinstance(value, (int, dbus.Int64)):\n\t\t\t\titemType = 'i'\n\t\t\telif isinstance(value, float):\n\t\t\t\titemType = 'f'\n\t\t\telse:\n\t\t\t\titemType = 's'\n\n\t\t\t# Add the setting\n\t\t\t# TODO, make an object that inherits VeDbusItemImport, and complete the D-Bus settingsitem interface\n\t\t\tsettings_item = VeDbusItemImport(self._bus, self._dbus_name, '/Settings', createsignal=False)\n\t\t\tsetting_path = path.replace('/Settings/', '', 1)\n\t\t\tif silent:\n\t\t\t\tsettings_item._proxy.AddSilentSetting('', setting_path, value, itemType, _min, _max)\n\t\t\telse:\n\t\t\t\tsettings_item._proxy.AddSetting('', setting_path, value, itemType, _min, _max)\n\n\t\t\tbusitem = VeDbusItemImport(self._bus, self._dbus_name, path, callback)\n\n\t\treturn busitem\n\n\tdef handleChangedSetting(self, setting, servicename, path, changes):\n\t\toldvalue = self._values[setting] if setting in self._values else None\n\t\tself._values[setting] = changes['Value']\n\n\t\tif self._eventCallback is None:\n\t\t\treturn\n\n\t\tself._eventCallback(setting, oldvalue, changes['Value'])\n\n\tdef setDefault(self, path):\n                item = VeDbusItemImport(self._bus, self._dbus_name, path, createsignal=False)\n                item.set_default()\n\n\tdef __getitem__(self, setting):\n\t\treturn self._settings[setting].get_value()\n\n\tdef __setitem__(self, setting, newvalue):\n\t\tresult = self._settings[setting].set_value(newvalue)\n\t\tif result != 0:\n\t\t\t# Trying to make some false change to our own settings? How dumb!\n\t\t\tassert False\n"
  },
  {
    "path": "velib_python/velib_python/latest/ve_utils.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\nimport sys\nfrom traceback import print_exc\nfrom os import _exit as os_exit\nfrom os import statvfs\nfrom subprocess import check_output, CalledProcessError\nimport logging\nimport dbus\nlogger = logging.getLogger(__name__)\n\nVEDBUS_INVALID = dbus.Array([], signature=dbus.Signature('i'), variant_level=1)\n\nclass NoVrmPortalIdError(Exception):\n\tpass\n\n# Use this function to make sure the code quits on an unexpected exception. Make sure to use it\n# when using GLib.idle_add and also GLib.timeout_add.\n# Without this, the code will just keep running, since GLib does not stop the mainloop on an\n# exception.\n# Example: GLib.idle_add(exit_on_error, myfunc, arg1, arg2)\ndef exit_on_error(func, *args, **kwargs):\n\ttry:\n\t\treturn func(*args, **kwargs)\n\texcept:\n\t\ttry:\n\t\t\tprint ('exit_on_error: there was an exception. Printing stacktrace will be tried and then exit')\n\t\t\tprint_exc()\n\t\texcept:\n\t\t\tpass\n\n\t\t# sys.exit() is not used, since that throws an exception, which does not lead to a program\n\t\t# halt when used in a dbus callback, see connection.py in the Python/Dbus libraries, line 230.\n\t\tos_exit(1)\n\n\n__vrm_portal_id = None\ndef get_vrm_portal_id():\n\t# The original definition of the VRM Portal ID is that it is the mac\n\t# address of the onboard- ethernet port (eth0), stripped from its colons\n\t# (:) and lower case. This may however differ between platforms. On Venus\n\t# the task is therefore deferred to /sbin/get-unique-id so that a\n\t# platform specific method can be easily defined.\n\t#\n\t# If /sbin/get-unique-id does not exist, then use the ethernet address\n\t# of eth0. This also handles the case where velib_python is used as a\n\t# package install on a Raspberry Pi.\n\t#\n\t# On a Linux host where the network interface may not be eth0, you can set\n\t# the VRM_IFACE environment variable to the correct name.\n\n\tglobal __vrm_portal_id\n\n\tif __vrm_portal_id:\n\t\treturn __vrm_portal_id\n\n\tportal_id = None\n\n\t# First try the method that works if we don't have a data partition. This\n\t# will fail when the current user is not root.\n\ttry:\n\t\tportal_id = check_output(\"/sbin/get-unique-id\").decode(\"utf-8\", \"ignore\").strip()\n\t\tif not portal_id:\n\t\t\traise NoVrmPortalIdError(\"get-unique-id returned blank\")\n\t\t__vrm_portal_id = portal_id\n\t\treturn portal_id\n\texcept CalledProcessError:\n\t\t# get-unique-id returned non-zero\n\t\traise NoVrmPortalIdError(\"get-unique-id returned non-zero\")\n\texcept OSError:\n\t\t# File doesn't exist, use fallback\n\t\tpass\n\n\t# Fall back to getting our id using a syscall. Assume we are on linux.\n\t# Allow the user to override what interface is used using an environment\n\t# variable.\n\timport fcntl, socket, struct, os\n\n\tiface = os.environ.get('VRM_IFACE', 'eth0').encode('ascii')\n\ts = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)\n\ttry:\n\t\tinfo = fcntl.ioctl(s.fileno(), 0x8927,  struct.pack('256s', iface[:15]))\n\texcept IOError:\n\t\traise NoVrmPortalIdError(\"ioctl failed for eth0\")\n\n\t__vrm_portal_id = info[18:24].hex()\n\treturn __vrm_portal_id\n\n\n# See VE.Can registers - public.docx for definition of this conversion\ndef convert_vreg_version_to_readable(version):\n\tdef str_to_arr(x, length):\n\t\ta = []\n\t\tfor i in range(0, len(x), length):\n\t\t\ta.append(x[i:i+length])\n\t\treturn a\n\n\tx = \"%x\" % version\n\tx = x.upper()\n\n\tif len(x) == 5 or len(x) == 3 or len(x) == 1:\n\t\tx = '0' + x\n\n\ta = str_to_arr(x, 2);\n\n\t# remove the first 00 if there are three bytes and it is 00\n\tif len(a) == 3 and a[0] == '00':\n\t\ta.remove(0);\n\n\t# if we have two or three bytes now, and the first character is a 0, remove it\n\tif len(a) >= 2 and a[0][0:1] == '0':\n\t\ta[0] = a[0][1];\n\n\tresult = ''\n\tfor item in a:\n\t\tresult += ('.' if result != '' else '') + item\n\n\n\tresult = 'v' + result\n\n\treturn result\n\n\ndef get_free_space(path):\n\tresult = -1\n\n\ttry:\n\t\ts = statvfs(path)\n\t\tresult = s.f_frsize * s.f_bavail     # Number of free bytes that ordinary users\n\texcept Exception as ex:\n\t\tlogger.info(\"Error while retrieving free space for path %s: %s\" % (path, ex))\n\n\treturn result\n\n\ndef _get_sysfs_machine_name():\n\ttry:\n\t\twith open('/sys/firmware/devicetree/base/model', 'r') as f:\n\t\t\treturn f.read().rstrip('\\x00')\n\texcept IOError:\n\t\tpass\n\n\treturn None\n\n# Returns None if it cannot find a machine name. Otherwise returns the string\n# containing the name\ndef get_machine_name():\n\t# First try calling the venus utility script\n\ttry:\n\t\treturn check_output(\"/usr/bin/product-name\").strip().decode('UTF-8')\n\texcept (CalledProcessError, OSError):\n\t\tpass\n\n\t# Fall back to sysfs\n\tname = _get_sysfs_machine_name()\n\tif name is not None:\n\t\treturn name\n\n\t# Fall back to venus build machine name\n\ttry:\n\t\twith open('/etc/venus/machine', 'r', encoding='UTF-8') as f:\n\t\t\treturn f.read().strip()\n\texcept IOError:\n\t\tpass\n\n\treturn None\n\n\ndef get_product_id():\n\t\"\"\" Find the machine ID and return it. \"\"\"\n\n\t# First try calling the venus utility script\n\ttry:\n\t\treturn check_output(\"/usr/bin/product-id\").strip().decode('UTF-8')\n\texcept (CalledProcessError, OSError):\n\t\tpass\n\n\t# Fall back machine name mechanism\n\tname = _get_sysfs_machine_name()\n\treturn {\n\t\t'Color Control GX': 'C001',\n\t\t'Venus GX': 'C002',\n\t\t'Octo GX': 'C006',\n\t\t'EasySolar-II': 'C007',\n\t\t'MultiPlus-II': 'C008',\n\t\t'Maxi GX': 'C009',\n\t\t'Cerbo GX': 'C00A'\n\t}.get(name, 'C003') # C003 is Generic\n\n\n# Returns False if it cannot open the file. Otherwise returns its rstripped contents\ndef read_file(path):\n\tcontent = False\n\n\ttry:\n\t\twith open(path, 'r') as f:\n\t\t\tcontent = f.read().rstrip()\n\texcept Exception as ex:\n\t\tlogger.debug(\"Error while reading %s: %s\" % (path, ex))\n\n\treturn content\n\n\ndef wrap_dbus_value(value):\n\tif value is None:\n\t\treturn VEDBUS_INVALID\n\tif isinstance(value, float):\n\t\treturn dbus.Double(value, variant_level=1)\n\tif isinstance(value, bool):\n\t\treturn dbus.Boolean(value, variant_level=1)\n\tif isinstance(value, int):\n\t\ttry:\n\t\t\treturn dbus.Int32(value, variant_level=1)\n\t\texcept OverflowError:\n\t\t\treturn dbus.Int64(value, variant_level=1)\n\tif isinstance(value, str):\n\t\treturn dbus.String(value, variant_level=1)\n\tif isinstance(value, list):\n\t\tif len(value) == 0:\n\t\t\t# If the list is empty we cannot infer the type of the contents. So assume unsigned integer.\n\t\t\t# A (signed) integer is dangerous, because an empty list of signed integers is used to encode\n\t\t\t# an invalid value.\n\t\t\treturn dbus.Array([], signature=dbus.Signature('u'), variant_level=1)\n\t\treturn dbus.Array([wrap_dbus_value(x) for x in value], variant_level=1)\n\tif isinstance(value, dict):\n\t\t# Wrapping the keys of the dictionary causes D-Bus errors like:\n\t\t# 'arguments to dbus_message_iter_open_container() were incorrect,\n\t\t# assertion \"(type == DBUS_TYPE_ARRAY && contained_signature &&\n\t\t# *contained_signature == DBUS_DICT_ENTRY_BEGIN_CHAR) || (contained_signature == NULL ||\n\t\t# _dbus_check_is_valid_signature (contained_signature))\" failed in file ...'\n\t\treturn dbus.Dictionary({(k, wrap_dbus_value(v)) for k, v in value.items()}, variant_level=1)\n\treturn value\n\n\ndbus_int_types = (dbus.Int32, dbus.UInt32, dbus.Byte, dbus.Int16, dbus.UInt16, dbus.UInt32, dbus.Int64, dbus.UInt64)\n\n\ndef unwrap_dbus_value(val):\n\t\"\"\"Converts D-Bus values back to the original type. For example if val is of type DBus.Double,\n\ta float will be returned.\"\"\"\n\tif isinstance(val, dbus_int_types):\n\t\treturn int(val)\n\tif isinstance(val, dbus.Double):\n\t\treturn float(val)\n\tif isinstance(val, dbus.Array):\n\t\tv = [unwrap_dbus_value(x) for x in val]\n\t\treturn None if len(v) == 0 else v\n\tif isinstance(val, (dbus.Signature, dbus.String)):\n\t\treturn str(val)\n\t# Python has no byte type, so we convert to an integer.\n\tif isinstance(val, dbus.Byte):\n\t\treturn int(val)\n\tif isinstance(val, dbus.ByteArray):\n\t\treturn \"\".join([bytes(x) for x in val])\n\tif isinstance(val, (list, tuple)):\n\t\treturn [unwrap_dbus_value(x) for x in val]\n\tif isinstance(val, (dbus.Dictionary, dict)):\n\t\t# Do not unwrap the keys, see comment in wrap_dbus_value\n\t\treturn dict([(x, unwrap_dbus_value(y)) for x, y in val.items()])\n\tif isinstance(val, dbus.Boolean):\n\t\treturn bool(val)\n\treturn val\n\n# When supported, only name owner changes for the the given namespace are reported. This\n# prevents spending cpu time at irrelevant changes, like scripts accessing the bus temporarily.\ndef add_name_owner_changed_receiver(dbus, name_owner_changed, namespace=\"com.victronenergy\"):\n\t# support for arg0namespace is submitted upstream, but not included at the time of\n\t# writing, Venus OS does support it, so try if it works.\n\tif namespace is None:\n\t\tdbus.add_signal_receiver(name_owner_changed, signal_name='NameOwnerChanged')\n\telse:\n\t\ttry:\n\t\t\tdbus.add_signal_receiver(name_owner_changed,\n\t\t\t\tsignal_name='NameOwnerChanged', arg0namespace=namespace)\n\t\texcept TypeError:\n\t\t\tdbus.add_signal_receiver(name_owner_changed, signal_name='NameOwnerChanged')\n"
  },
  {
    "path": "velib_python/velib_python/latest/vedbus.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\nimport dbus.service\nimport logging\nimport traceback\nimport os\nimport weakref\nfrom collections import defaultdict\nfrom ve_utils import wrap_dbus_value, unwrap_dbus_value\n\n# vedbus contains three classes:\n# VeDbusItemImport -> use this to read data from the dbus, ie import\n# VeDbusItemExport -> use this to export data to the dbus (one value)\n# VeDbusService -> use that to create a service and export several values to the dbus\n\n# Code for VeDbusItemImport is copied from busitem.py and thereafter modified.\n# All projects that used busitem.py need to migrate to this package. And some\n# projects used to define there own equivalent of VeDbusItemExport. Better to\n# use VeDbusItemExport, or even better the VeDbusService class that does it all for you.\n\n# TODOS\n# 1 check for datatypes, it works now, but not sure if all is compliant with\n#\tcom.victronenergy.BusItem interface definition. See also the files in\n#\ttests_and_examples. And see 'if type(v) == dbus.Byte:' on line 102. Perhaps\n#\tsomething similar should also be done in VeDbusBusItemExport?\n# 2 Shouldn't VeDbusBusItemExport inherit dbus.service.Object?\n# 7 Make hard rules for services exporting data to the D-Bus, in order to make tracking\n#   changes possible. Does everybody first invalidate its data before leaving the bus?\n#   And what about before taking one object away from the bus, instead of taking the\n#   whole service offline?\n#   They should! And after taking one value away, do we need to know that someone left\n#   the bus? Or we just keep that value in invalidated for ever? Result is that we can't\n#   see the difference anymore between an invalidated value and a value that was first on\n#   the bus and later not anymore. See comments above VeDbusItemImport as well.\n# 9 there are probably more todos in the code below.\n\n# Some thoughts with regards to the data types:\n#\n#   Text from: http://dbus.freedesktop.org/doc/dbus-python/doc/tutorial.html#data-types\n#   ---\n#   Variants are represented by setting the variant_level keyword argument in the\n#   constructor of any D-Bus data type to a value greater than 0 (variant_level 1\n#   means a variant containing some other data type, variant_level 2 means a variant\n#   containing a variant containing some other data type, and so on). If a non-variant\n#   is passed as an argument but introspection indicates that a variant is expected,\n#   it'll automatically be wrapped in a variant.\n#   ---\n#\n#   Also the different dbus datatypes, such as dbus.Int32, and dbus.UInt32 are a subclass\n#   of Python int. dbus.String is a subclass of Python standard class unicode, etcetera\n#\n#   So all together that explains why we don't need to explicitly convert back and forth\n#   between the dbus datatypes and the standard python datatypes. Note that all datatypes\n#   in python are objects. Even an int is an object.\n\n#   The signature of a variant is 'v'.\n\n# Export ourselves as a D-Bus service.\nclass VeDbusService(object):\n\tdef __init__(self, servicename, bus=None, register=True):\n\t\t# dict containing the VeDbusItemExport objects, with their path as the key.\n\t\tself._dbusobjects = {}\n\t\tself._dbusnodes = {}\n\t\tself._ratelimiters = []\n\t\tself._dbusname = None\n\t\tself.name = servicename\n\n\t\t# dict containing the onchange callbacks, for each object. Object path is the key\n\t\tself._onchangecallbacks = {}\n\n\t\t# Connect to session bus whenever present, else use the system bus\n\t\tself._dbusconn = bus or (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else dbus.SystemBus())\n\n\t\t# make the dbus connection available to outside, could make this a true property instead, but ach..\n\t\tself.dbusconn = self._dbusconn\n\n\t\t# Add the root item that will return all items as a tree\n\t\tself._dbusnodes['/'] = VeDbusRootExport(self._dbusconn, '/', self)\n\n\t\t# Immediately register the service unless requested not to\n\t\tif register:\n\t\t\tlogging.warning(\"USING OUTDATED REGISTRATION METHOD!\")\n\t\t\tlogging.warning(\"Please set register=False, then call the register method \"\n\t\t\t\t\"after adding all mandatory paths. See \"\n\t\t\t\t\"https://github.com/victronenergy/venus/wiki/dbus-api\")\n\t\t\tself.register()\n\n\tdef register(self):\n\t\t# Register ourselves on the dbus, trigger an error if already in use (do_not_queue)\n\t\tself._dbusname = dbus.service.BusName(self.name, self._dbusconn, do_not_queue=True)\n\t\tlogging.info(\"registered ourselves on D-Bus as %s\" % self.name)\n\n\t# To force immediate deregistering of this dbus service and all its object paths, explicitly\n\t# call __del__().\n\tdef __del__(self):\n\t\tfor node in list(self._dbusnodes.values()):\n\t\t\tnode.__del__()\n\t\tself._dbusnodes.clear()\n\t\tfor item in list(self._dbusobjects.values()):\n\t\t\titem.__del__()\n\t\tself._dbusobjects.clear()\n\t\tif self._dbusname:\n\t\t\tself._dbusname.__del__()  # Forces call to self._bus.release_name(self._name), see source code\n\t\tself._dbusname = None\n\n\tdef get_name(self):\n\t\treturn self._dbusname.get_name()\n\n\t# @param callbackonchange\tfunction that will be called when this value is changed. First parameter will\n\t#\t\t\t\t\t\t\tbe the path of the object, second the new value. This callback should return\n\t#\t\t\t\t\t\t\tTrue to accept the change, False to reject it.\n\tdef add_path(self, path, value, description=\"\", writeable=False,\n\t\t\t\t\tonchangecallback=None, gettextcallback=None, valuetype=None, itemtype=None):\n\n\t\tif onchangecallback is not None:\n\t\t\tself._onchangecallbacks[path] = onchangecallback\n\n\t\titemtype = itemtype or VeDbusItemExport\n\t\titem = itemtype(self._dbusconn, path, value, description, writeable,\n\t\t\t\tself._value_changed, gettextcallback, deletecallback=self._item_deleted, valuetype=valuetype)\n\n\t\tspl = path.split('/')\n\t\tfor i in range(2, len(spl)):\n\t\t\tsubPath = '/'.join(spl[:i])\n\t\t\tif subPath not in self._dbusnodes and subPath not in self._dbusobjects:\n\t\t\t\tself._dbusnodes[subPath] = VeDbusTreeExport(self._dbusconn, subPath, self)\n\t\tself._dbusobjects[path] = item\n\t\tlogging.debug('added %s with start value %s. Writeable is %s' % (path, value, writeable))\n\t\treturn item\n\n\t# Add the mandatory paths, as per victron dbus api doc\n\tdef add_mandatory_paths(self, processname, processversion, connection,\n\t\t\tdeviceinstance, productid, productname, firmwareversion, hardwareversion, connected):\n\t\tself.add_path('/Mgmt/ProcessName', processname)\n\t\tself.add_path('/Mgmt/ProcessVersion', processversion)\n\t\tself.add_path('/Mgmt/Connection', connection)\n\n\t\t# Create rest of the mandatory objects\n\t\tself.add_path('/DeviceInstance', deviceinstance)\n\t\tself.add_path('/ProductId', productid)\n\t\tself.add_path('/ProductName', productname)\n\t\tself.add_path('/FirmwareVersion', firmwareversion)\n\t\tself.add_path('/HardwareVersion', hardwareversion)\n\t\tself.add_path('/Connected', connected)\n\n\t# Callback function that is called from the VeDbusItemExport objects when a value changes. This function\n\t# maps the change-request to the onchangecallback given to us for this specific path.\n\tdef _value_changed(self, path, newvalue):\n\t\tif path not in self._onchangecallbacks:\n\t\t\treturn True\n\n\t\treturn self._onchangecallbacks[path](path, newvalue)\n\n\tdef _item_deleted(self, path):\n\t\tself._dbusobjects.pop(path)\n\t\tfor np in list(self._dbusnodes.keys()):\n\t\t\tif np != '/':\n\t\t\t\tfor ip in self._dbusobjects:\n\t\t\t\t\tif ip.startswith(np + '/'):\n\t\t\t\t\t\tbreak\n\t\t\t\telse:\n\t\t\t\t\tself._dbusnodes[np].__del__()\n\t\t\t\t\tself._dbusnodes.pop(np)\n\n\tdef __getitem__(self, path):\n\t\treturn self._dbusobjects[path].local_get_value()\n\n\tdef __setitem__(self, path, newvalue):\n\t\tself._dbusobjects[path].local_set_value(newvalue)\n\n\tdef __delitem__(self, path):\n\t\tself._dbusobjects[path].__del__()  # Invalidates and then removes the object path\n\t\tassert path not in self._dbusobjects\n\n\tdef __contains__(self, path):\n\t\treturn path in self._dbusobjects\n\n\tdef __enter__(self):\n\t\tl = ServiceContext(self)\n\t\tself._ratelimiters.append(l)\n\t\treturn l\n\n\tdef __exit__(self, *exc):\n\t\t# pop off the top one and flush it. If with statements are nested\n\t\t# then each exit flushes its own part.\n\t\tif self._ratelimiters:\n\t\t\tself._ratelimiters.pop().flush()\n\nclass ServiceContext(object):\n\tdef __init__(self, parent):\n\t\tself.parent = parent\n\t\tself.changes = {}\n\n\tdef __contains__(self, path):\n\t\treturn path in self.parent\n\n\tdef __getitem__(self, path):\n\t\treturn self.parent[path]\n\n\tdef __setitem__(self, path, newvalue):\n\t\tc = self.parent._dbusobjects[path]._local_set_value(newvalue)\n\t\tif c is not None:\n\t\t\tself.changes[path] = c\n\n\tdef __delitem__(self, path):\n\t\tif path in self.changes:\n\t\t\tdel self.changes[path]\n\t\tdel self.parent[path]\n\n\tdef flush(self):\n\t\tif self.changes:\n\t\t\tself.parent._dbusnodes['/'].ItemsChanged(self.changes)\n\t\t\tself.changes.clear()\n\n\tdef add_path(self, path, value, *args, **kwargs):\n\t\tself.parent.add_path(path, value, *args, **kwargs)\n\t\tself.changes[path] = {\n\t\t\t'Value': wrap_dbus_value(value),\n\t\t\t'Text': self.parent._dbusobjects[path].GetText()\n\t\t}\n\n\tdef del_tree(self, root):\n\t\troot = root.rstrip('/')\n\t\tfor p in list(self.parent._dbusobjects.keys()):\n\t\t\tif p == root or p.startswith(root + '/'):\n\t\t\t\tself[p] = None\n\t\t\t\tself.parent._dbusobjects[p].__del__()\n\n\tdef get_name(self):\n\t\treturn self.parent.get_name()\n\nclass TrackerDict(defaultdict):\n\t\"\"\" Same as defaultdict, but passes the key to default_factory. \"\"\"\n\tdef __missing__(self, key):\n\t\tself[key] = x = self.default_factory(key)\n\t\treturn x\n\nclass VeDbusRootTracker(object):\n\t\"\"\" This tracks the root of a dbus path and listens for PropertiesChanged\n\t    signals. When a signal arrives, parse it and unpack the key/value changes\n\t    into traditional events, then pass it to the original eventCallback\n\t    method. \"\"\"\n\tdef __init__(self, bus, serviceName):\n\t\tself.importers = defaultdict(weakref.WeakSet)\n\t\tself.serviceName = serviceName\n\t\tself._match = bus.get_object(serviceName, '/', introspect=False).connect_to_signal(\n\t\t\t\"ItemsChanged\", weak_functor(self._items_changed_handler))\n\n\tdef __del__(self):\n\t\tself._match.remove()\n\t\tself._match = None\n\n\tdef add(self, i):\n\t\tself.importers[i.path].add(i)\n\n\tdef _items_changed_handler(self, items):\n\t\tif not isinstance(items, dict):\n\t\t\treturn\n\n\t\tfor path, changes in items.items():\n\t\t\ttry:\n\t\t\t\tv = changes['Value']\n\t\t\texcept KeyError:\n\t\t\t\tcontinue\n\n\t\t\ttry:\n\t\t\t\tt = changes['Text']\n\t\t\texcept KeyError:\n\t\t\t\tt = str(unwrap_dbus_value(v))\n\n\t\t\tfor i in self.importers.get(path, ()):\n\t\t\t\ti._properties_changed_handler({'Value': v, 'Text': t})\n\n\"\"\"\nImporting basics:\n\t- If when we power up, the D-Bus service does not exist, or it does exist and the path does not\n\t  yet exist, still subscribe to a signal: as soon as it comes online it will send a signal with its\n\t  initial value, which VeDbusItemImport will receive and use to update local cache. And, when set,\n\t  call the eventCallback.\n\t- If when we power up, save it\n\t- When using get_value, know that there is no difference between services (or object paths) that don't\n\t  exist and paths that are invalid (= empty array, see above). Both will return None. In case you do\n\t  really want to know ifa path exists or not, use the exists property.\n\t- When a D-Bus service leaves the D-Bus, it will first invalidate all its values, and send signals\n\t  with that update, and only then leave the D-Bus. (or do we need to subscribe to the NameOwnerChanged-\n\t  signal!?!) To be discussed and make sure. Not really urgent, since all existing code that uses this\n\t  class already subscribes to the NameOwnerChanged signal, and subsequently removes instances of this\n\t  class.\n\nRead when using this class:\nNote that when a service leaves that D-Bus without invalidating all its exported objects first, for\nexample because it is killed, VeDbusItemImport doesn't have a clue. So when using VeDbusItemImport,\nmake sure to also subscribe to the NamerOwnerChanged signal on bus-level. Or just use dbusmonitor,\nbecause that takes care of all of that for you.\n\"\"\"\nclass VeDbusItemImport(object):\n\tdef __new__(cls, bus, serviceName, path, eventCallback=None, createsignal=True):\n\t\tinstance = object.__new__(cls)\n\n\t\t# If signal tracking should be done, also add to root tracker\n\t\tif createsignal:\n\t\t\tif \"_roots\" not in cls.__dict__:\n\t\t\t\tcls._roots = TrackerDict(lambda k: VeDbusRootTracker(bus, k))\n\n\t\treturn instance\n\n\t## Constructor\n\t# @param bus\t\t\tthe bus-object (SESSION or SYSTEM).\n\t# @param serviceName\tthe dbus-service-name (string), for example 'com.victronenergy.battery.ttyO1'\n\t# @param path\t\t\tthe object-path, for example '/Dc/V'\n\t# @param eventCallback\tfunction that you want to be called on a value change\n\t# @param createSignal   only set this to False if you use this function to one time read a value. When\n\t#\t\t\t\t\t\tleaving it to True, make sure to also subscribe to the NameOwnerChanged signal\n\t#\t\t\t\t\t\telsewhere. See also note some 15 lines up.\n\tdef __init__(self, bus, serviceName, path, eventCallback=None, createsignal=True):\n\t\t# TODO: is it necessary to store _serviceName and _path? Isn't it\n\t\t# stored in the bus_getobjectsomewhere?\n\t\tself._serviceName = serviceName\n\t\tself._path = path\n\t\tself._match = None\n\t\t# TODO: _proxy is being used in settingsdevice.py, make a getter for that\n\t\tself._proxy = bus.get_object(serviceName, path, introspect=False)\n\t\tself.eventCallback = eventCallback\n\n\t\tassert eventCallback is None or createsignal == True\n\t\tif createsignal:\n\t\t\tself._match = self._proxy.connect_to_signal(\n\t\t\t\t\"PropertiesChanged\", weak_functor(self._properties_changed_handler))\n\t\t\tself._roots[serviceName].add(self)\n\n\t\t# store the current value in _cachedvalue. When it doesn't exists set _cachedvalue to\n\t\t# None, same as when a value is invalid\n\t\tself._cachedvalue = None\n\t\ttry:\n\t\t\tv = self._proxy.GetValue()\n\t\texcept dbus.exceptions.DBusException:\n\t\t\tpass\n\t\telse:\n\t\t\tself._cachedvalue = unwrap_dbus_value(v)\n\n\tdef __del__(self):\n\t\tif self._match is not None:\n\t\t\tself._match.remove()\n\t\t\tself._match = None\n\t\tself._proxy = None\n\n\tdef _refreshcachedvalue(self):\n\t\tself._cachedvalue = unwrap_dbus_value(self._proxy.GetValue())\n\n\t## Returns the path as a string, for example '/AC/L1/V'\n\t@property\n\tdef path(self):\n\t\treturn self._path\n\n\t## Returns the dbus service name as a string, for example com.victronenergy.vebus.ttyO1\n\t@property\n\tdef serviceName(self):\n\t\treturn self._serviceName\n\n\t## Returns the value of the dbus-item.\n\t# the type will be a dbus variant, for example dbus.Int32(0, variant_level=1)\n\t# this is not a property to keep the name consistant with the com.victronenergy.busitem interface\n\t# returns None when the property is invalid\n\tdef get_value(self):\n\t\treturn self._cachedvalue\n\n\t## Writes a new value to the dbus-item\n\tdef set_value(self, newvalue):\n\t\tr = self._proxy.SetValue(wrap_dbus_value(newvalue))\n\n\t\t# instead of just saving the value, go to the dbus and get it. So we have the right type etc.\n\t\tif r == 0:\n\t\t\tself._refreshcachedvalue()\n\n\t\treturn r\n\n\t## Resets the item to its default value\n\tdef set_default(self):\n\t\tself._proxy.SetDefault()\n\t\tself._refreshcachedvalue()\n\n\t## Returns the text representation of the value.\n\t# For example when the value is an enum/int GetText might return the string\n\t# belonging to that enum value. Another example, for a voltage, GetValue\n\t# would return a float, 12.0Volt, and GetText could return 12 VDC.\n\t#\n\t# Note that this depends on how the dbus-producer has implemented this.\n\tdef get_text(self):\n\t\treturn self._proxy.GetText()\n\n\t## Returns true of object path exists, and false if it doesn't\n\t@property\n\tdef exists(self):\n\t\t# TODO: do some real check instead of this crazy thing.\n\t\tr = False\n\t\ttry:\n\t\t\tr = self._proxy.GetValue()\n\t\t\tr = True\n\t\texcept dbus.exceptions.DBusException:\n\t\t\tpass\n\n\t\treturn r\n\n\t## callback for the trigger-event.\n\t# @param eventCallback the event-callback-function.\n\t@property\n\tdef eventCallback(self):\n\t\treturn self._eventCallback\n\n\t@eventCallback.setter\n\tdef eventCallback(self, eventCallback):\n\t\tself._eventCallback = eventCallback\n\n\t## Is called when the value of the imported bus-item changes.\n\t# Stores the new value in our local cache, and calls the eventCallback, if set.\n\tdef _properties_changed_handler(self, changes):\n\t\tif \"Value\" in changes:\n\t\t\tchanges['Value'] = unwrap_dbus_value(changes['Value'])\n\t\t\tself._cachedvalue = changes['Value']\n\t\t\tif self._eventCallback:\n\t\t\t\t# The reason behind this try/except is to prevent errors silently ending up the an error\n\t\t\t\t# handler in the dbus code.\n\t\t\t\ttry:\n\t\t\t\t\tself._eventCallback(self._serviceName, self._path, changes)\n\t\t\t\texcept:\n\t\t\t\t\ttraceback.print_exc()\n\t\t\t\t\tos._exit(1)  # sys.exit() is not used, since that also throws an exception\n\n\nclass VeDbusTreeExport(dbus.service.Object):\n\tdef __init__(self, bus, objectPath, service):\n\t\tdbus.service.Object.__init__(self, bus, objectPath)\n\t\tself._service = service\n\t\tlogging.debug(\"VeDbusTreeExport %s has been created\" % objectPath)\n\n\tdef __del__(self):\n\t\t# self._get_path() will raise an exception when retrieved after the call to .remove_from_connection,\n\t\t# so we need a copy.\n\t\tpath = self._get_path()\n\t\tif path is None:\n\t\t\treturn\n\t\tself.remove_from_connection()\n\t\tlogging.debug(\"VeDbusTreeExport %s has been removed\" % path)\n\n\tdef _get_path(self):\n\t\tif len(self._locations) == 0:\n\t\t\treturn None\n\t\treturn self._locations[0][1]\n\n\tdef _get_value_handler(self, path, get_text=False):\n\t\tlogging.debug(\"_get_value_handler called for %s\" % path)\n\t\tr = {}\n\t\tpx = path\n\t\tif not px.endswith('/'):\n\t\t\tpx += '/'\n\t\tfor p, item in self._service._dbusobjects.items():\n\t\t\tif p.startswith(px):\n\t\t\t\tv = item.GetText() if get_text else wrap_dbus_value(item.local_get_value())\n\t\t\t\tr[p[len(px):]] = v\n\t\tlogging.debug(r)\n\t\treturn r\n\n\t@dbus.service.method('com.victronenergy.BusItem', out_signature='v')\n\tdef GetValue(self):\n\t\tvalue = self._get_value_handler(self._get_path())\n\t\treturn dbus.Dictionary(value, signature=dbus.Signature('sv'), variant_level=1)\n\n\t@dbus.service.method('com.victronenergy.BusItem', out_signature='v')\n\tdef GetText(self):\n\t\treturn self._get_value_handler(self._get_path(), True)\n\n\tdef local_get_value(self):\n\t\treturn self._get_value_handler(self.path)\n\nclass VeDbusRootExport(VeDbusTreeExport):\n\t@dbus.service.signal('com.victronenergy.BusItem', signature='a{sa{sv}}')\n\tdef ItemsChanged(self, changes):\n\t\tpass\n\n\t@dbus.service.method('com.victronenergy.BusItem', out_signature='a{sa{sv}}')\n\tdef GetItems(self):\n\t\treturn {\n\t\t\tpath: {\n\t\t\t\t'Value': wrap_dbus_value(item.local_get_value()),\n\t\t\t\t'Text': item.GetText() }\n\t\t\tfor path, item in self._service._dbusobjects.items()\n\t\t}\n\n\nclass VeDbusItemExport(dbus.service.Object):\n\t## Constructor of VeDbusItemExport\n\t#\n\t# Use this object to export (publish), values on the dbus\n\t# Creates the dbus-object under the given dbus-service-name.\n\t# @param bus\t\t  The dbus object.\n\t# @param objectPath\t  The dbus-object-path.\n\t# @param value\t\t  Value to initialize ourselves with, defaults to None which means Invalid\n\t# @param description  String containing a description. Can be called over the dbus with GetDescription()\n\t# @param writeable\t  what would this do!? :).\n\t# @param callback\t  Function that will be called when someone else changes the value of this VeBusItem\n\t#                     over the dbus. First parameter passed to callback will be our path, second the new\n\t#\t\t\t\t\t  value. This callback should return True to accept the change, False to reject it.\n\tdef __init__(self, bus, objectPath, value=None, description=None, writeable=False,\n\t\t\t\t\tonchangecallback=None, gettextcallback=None, deletecallback=None,\n\t\t\t\t\tvaluetype=None):\n\t\tdbus.service.Object.__init__(self, bus, objectPath)\n\t\tself._onchangecallback = onchangecallback\n\t\tself._gettextcallback = gettextcallback\n\t\tself._value = value\n\t\tself._description = description\n\t\tself._writeable = writeable\n\t\tself._deletecallback = deletecallback\n\t\tself._type = valuetype\n\n\t# To force immediate deregistering of this dbus object, explicitly call __del__().\n\tdef __del__(self):\n\t\t# self._get_path() will raise an exception when retrieved after the\n\t\t# call to .remove_from_connection, so we need a copy.\n\t\tpath = self._get_path()\n\t\tif path == None:\n\t\t\treturn\n\t\tif self._deletecallback is not None:\n\t\t\tself._deletecallback(path)\n\t\tself.remove_from_connection()\n\t\tlogging.debug(\"VeDbusItemExport %s has been removed\" % path)\n\n\tdef _get_path(self):\n\t\tif len(self._locations) == 0:\n\t\t\treturn None\n\t\treturn self._locations[0][1]\n\n\t## Sets the value. And in case the value is different from what it was, a signal\n\t# will be emitted to the dbus. This function is to be used in the python code that\n\t# is using this class to export values to the dbus.\n\t# set value to None to indicate that it is Invalid\n\tdef local_set_value(self, newvalue):\n\t\tchanges = self._local_set_value(newvalue)\n\t\tif changes is not None:\n\t\t\tself.PropertiesChanged(changes)\n\n\tdef _local_set_value(self, newvalue):\n\t\tif self._value == newvalue:\n\t\t\treturn None\n\n\t\tself._value = newvalue\n\t\treturn {\n\t\t\t'Value': wrap_dbus_value(newvalue),\n\t\t\t'Text': self.GetText()\n\t\t}\n\n\tdef local_get_value(self):\n\t\treturn self._value\n\n\t# ==== ALL FUNCTIONS BELOW THIS LINE WILL BE CALLED BY OTHER PROCESSES OVER THE DBUS ====\n\n\t## Dbus exported method SetValue\n\t# Function is called over the D-Bus by other process. It will first check (via callback) if new\n\t# value is accepted. And it is, stores it and emits a changed-signal.\n\t# @param value The new value.\n\t# @return completion-code When successful a 0 is return, and when not a -1 is returned.\n\t@dbus.service.method('com.victronenergy.BusItem', in_signature='v', out_signature='i')\n\tdef SetValue(self, newvalue):\n\t\tif not self._writeable:\n\t\t\treturn 1  # NOT OK\n\n\t\tnewvalue = unwrap_dbus_value(newvalue)\n\n\t\t# If value type is enforced, cast it. If the type can be coerced\n\t\t# python will do it for us. This allows ints to become floats,\n\t\t# or bools to become ints. Additionally also allow None, so that\n\t\t# a path may be invalidated.\n\t\tif self._type is not None and newvalue is not None:\n\t\t\ttry:\n\t\t\t\tnewvalue = self._type(newvalue)\n\t\t\texcept (ValueError, TypeError):\n\t\t\t\treturn 1 # NOT OK\n\n\t\tif newvalue == self._value:\n\t\t\treturn 0  # OK\n\n\t\t# call the callback given to us, and check if new value is OK.\n\t\tif (self._onchangecallback is None or\n\t\t\t\t(self._onchangecallback is not None and self._onchangecallback(self.__dbus_object_path__, newvalue))):\n\n\t\t\tself.local_set_value(newvalue)\n\t\t\treturn 0  # OK\n\n\t\treturn 2  # NOT OK\n\n\t## Dbus exported method GetDescription\n\t#\n\t# Returns the a description.\n\t# @param language A language code (e.g. ISO 639-1 en-US).\n\t# @param length Lenght of the language string.\n\t# @return description\n\t@dbus.service.method('com.victronenergy.BusItem', in_signature='si', out_signature='s')\n\tdef GetDescription(self, language, length):\n\t\treturn self._description if self._description is not None else 'No description given'\n\n\t## Dbus exported method GetValue\n\t# Returns the value.\n\t# @return the value when valid, and otherwise an empty array\n\t@dbus.service.method('com.victronenergy.BusItem', out_signature='v')\n\tdef GetValue(self):\n\t\treturn wrap_dbus_value(self._value)\n\n\t## Dbus exported method GetText\n\t# Returns the value as string of the dbus-object-path.\n\t# @return text A text-value. '---' when local value is invalid\n\t@dbus.service.method('com.victronenergy.BusItem', out_signature='s')\n\tdef GetText(self):\n\t\tif self._value is None:\n\t\t\treturn '---'\n\n\t\t# Default conversion from dbus.Byte will get you a character (so 'T' instead of '84'), so we\n\t\t# have to convert to int first. Note that if a dbus.Byte turns up here, it must have come from\n\t\t# the application itself, as all data from the D-Bus should have been unwrapped by now.\n\t\tif self._gettextcallback is None and type(self._value) == dbus.Byte:\n\t\t\treturn str(int(self._value))\n\n\t\tif self._gettextcallback is None and self.__dbus_object_path__ == '/ProductId':\n\t\t\treturn \"0x%X\" % self._value\n\n\t\tif self._gettextcallback is None:\n\t\t\treturn str(self._value)\n\n\t\treturn self._gettextcallback(self.__dbus_object_path__, self._value)\n\n\t## The signal that indicates that the value has changed.\n\t# Other processes connected to this BusItem object will have subscribed to the\n\t# event when they want to track our state.\n\t@dbus.service.signal('com.victronenergy.BusItem', signature='a{sv}')\n\tdef PropertiesChanged(self, changes):\n\t\tpass\n\n## This class behaves like a regular reference to a class method (eg. self.foo), but keeps a weak reference\n## to the object which method is to be called.\n## Use this object to break circular references.\nclass weak_functor:\n\tdef __init__(self, f):\n\t\tself._r = weakref.ref(f.__self__)\n\t\tself._f = weakref.ref(f.__func__)\n\n\tdef __call__(self, *args, **kargs):\n\t\tr = self._r()\n\t\tf = self._f()\n\t\tif r == None or f == None:\n\t\t\treturn\n\t\tf(r, *args, **kargs)\n"
  },
  {
    "path": "velib_python/velib_python/v3.34/dbusmonitor.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\n## @package dbus_vrm\n# This code takes care of the D-Bus interface (not all of below is implemented yet):\n# - on startup it scans the dbus for services we know. For each known service found, it searches for\n#   objects/paths we know. Everything we find is stored in items{}, and an event is registered: if a\n#   value changes weĺl be notified and can pass that on to our owner. For example the vrmLogger.\n#   we know.\n# - after startup, it continues to monitor the dbus:\n#\t\t1) when services are added we do the same check on that\n#\t\t2) when services are removed, we remove any items that we had that referred to that service\n#\t\t3) if an existing services adds paths we update ourselves as well: on init, we make a\n#\t\t   VeDbusItemImport for a non-, or not yet existing objectpaths as well1\n#\n# Code is used by the vrmLogger, and also the pubsub code. Both are other modules in the dbus_vrm repo.\n\nfrom dbus.mainloop.glib import DBusGMainLoop\nfrom gi.repository import GLib\nimport dbus\nimport dbus.service\nimport inspect\nimport logging\nimport argparse\nimport pprint\nimport traceback\nimport os\nfrom collections import defaultdict\nfrom functools import partial\n\n# our own packages\nfrom ve_utils import exit_on_error, wrap_dbus_value, unwrap_dbus_value\nnotfound = object() # For lookups where None is a valid result\n\nlogger = logging.getLogger(__name__)\nlogger.setLevel(logging.INFO)\nclass SystemBus(dbus.bus.BusConnection):\n\tdef __new__(cls):\n\t\treturn dbus.bus.BusConnection.__new__(cls, dbus.bus.BusConnection.TYPE_SYSTEM)\n\nclass SessionBus(dbus.bus.BusConnection):\n\tdef __new__(cls):\n\t\treturn dbus.bus.BusConnection.__new__(cls, dbus.bus.BusConnection.TYPE_SESSION)\n\nclass MonitoredValue(object):\n\tdef __init__(self, value, text, options):\n\t\tsuper(MonitoredValue, self).__init__()\n\t\tself.value = value\n\t\tself.text = text\n\t\tself.options = options\n\n\t# For legacy code, allow treating this as a tuple/list\n\tdef __iter__(self):\n\t\treturn iter((self.value, self.text, self.options))\n\nclass Service(object):\n\tdef __init__(self, id, serviceName, deviceInstance):\n\t\tsuper(Service, self).__init__()\n\t\tself.id = id\n\t\tself.name = serviceName\n\t\tself.paths = {}\n\t\tself._seen = set()\n\t\tself.deviceInstance = deviceInstance\n\n\t# For legacy code, attributes can still be accessed as if keys from a\n\t# dictionary.\n\tdef __setitem__(self, key, value):\n\t\tself.__dict__[key] = value\n\tdef __getitem__(self, key):\n\t\treturn self.__dict__[key]\n\n\tdef set_seen(self, path):\n\t\tself._seen.add(path)\n\n\tdef seen(self, path):\n\t\treturn path in self._seen\n\n\t@property\n\tdef service_class(self):\n\t\treturn '.'.join(self.name.split('.')[:3])\n\nclass DbusMonitor(object):\n\t## Constructor\n\tdef __init__(self, dbusTree, valueChangedCallback=None, deviceAddedCallback=None,\n\t\t\t\t\tdeviceRemovedCallback=None, namespace=\"com.victronenergy\"):\n\t\t# valueChangedCallback is the callback that we call when something has changed.\n\t\t# def value_changed_on_dbus(dbusServiceName, dbusPath, options, changes, deviceInstance):\n\t\t# in which changes is a tuple with GetText() and GetValue()\n\t\tself.valueChangedCallback = valueChangedCallback\n\t\tself.deviceAddedCallback = deviceAddedCallback\n\t\tself.deviceRemovedCallback = deviceRemovedCallback\n\t\tself.dbusTree = dbusTree\n\n\t\t# Lists all tracked services. Stores name, id, device instance, value per path, and whenToLog info\n\t\t# indexed by service name (eg. com.victronenergy.settings).\n\t\tself.servicesByName = {}\n\n\t\t# Same values as self.servicesByName, but indexed by service id (eg. :1.30)\n\t\tself.servicesById = {}\n\n\t\t# Keep track of services by class to speed up calls to get_service_list\n\t\tself.servicesByClass = defaultdict(list)\n\n\t\t# Keep track of any additional watches placed on items\n\t\tself.serviceWatches = defaultdict(list)\n\n\t\t# For a PC, connect to the SessionBus\n\t\t# For a CCGX, connect to the SystemBus\n\t\tself.dbusConn = SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else SystemBus()\n\n\t\t# subscribe to NameOwnerChange for bus connect / disconnect events.\n\t\t# NOTE: this is on a different bus then the one above!\n\t\tstandardBus = (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ \\\n\t\t\telse dbus.SystemBus())\n\n\t\tself.add_name_owner_changed_receiver(standardBus, self.dbus_name_owner_changed)\n\n\t\t# Subscribe to PropertiesChanged for all services\n\t\tself.dbusConn.add_signal_receiver(self.handler_value_changes,\n\t\t\tdbus_interface='com.victronenergy.BusItem',\n\t\t\tsignal_name='PropertiesChanged', path_keyword='path',\n\t\t\tsender_keyword='senderId')\n\n\t\t# Subscribe to ItemsChanged for all services\n\t\tself.dbusConn.add_signal_receiver(self.handler_item_changes,\n\t\t\tdbus_interface='com.victronenergy.BusItem',\n\t\t\tsignal_name='ItemsChanged', path='/',\n\t\t\tsender_keyword='senderId')\n\n\t\tlogger.info('===== Search on dbus for services that we will monitor starting... =====')\n\t\tserviceNames = self.dbusConn.list_names()\n\t\tfor serviceName in serviceNames:\n\t\t\tself.scan_dbus_service(serviceName)\n\n\t\tlogger.info('===== Search on dbus for services that we will monitor finished =====')\n\n\t@staticmethod\n\tdef make_service(serviceId, serviceName, deviceInstance):\n\t\t\"\"\" Override this to use a different kind of service object. \"\"\"\n\t\treturn Service(serviceId, serviceName, deviceInstance)\n\n\tdef make_monitor(self, service, path, value, text, options):\n\t\t\"\"\" Override this to do more things with monitoring. \"\"\"\n\t\treturn MonitoredValue(unwrap_dbus_value(value), unwrap_dbus_value(text), options)\n\n\tdef dbus_name_owner_changed(self, name, oldowner, newowner):\n\t\tif not name.startswith(\"com.victronenergy.\"):\n\t\t\treturn\n\n\t\t#decouple, and process in main loop\n\t\tGLib.idle_add(exit_on_error, self._process_name_owner_changed, name, oldowner, newowner)\n\n\t@staticmethod\n\t# When supported, only name owner changes for the the given namespace are reported. This\n\t# prevents spending cpu time at irrelevant changes, like scripts accessing the bus temporarily.\n\tdef add_name_owner_changed_receiver(dbus, name_owner_changed, namespace=\"com.victronenergy\"):\n\t\t# support for arg0namespace is submitted upstream, but not included at the time of\n\t\t# writing, Venus OS does support it, so try if it works.\n\t\tif namespace is None:\n\t\t\tdbus.add_signal_receiver(name_owner_changed, signal_name='NameOwnerChanged')\n\t\telse:\n\t\t\ttry:\n\t\t\t\tdbus.add_signal_receiver(name_owner_changed,\n\t\t\t\t\tsignal_name='NameOwnerChanged', arg0namespace=namespace)\n\t\t\texcept TypeError:\n\t\t\t\tdbus.add_signal_receiver(name_owner_changed, signal_name='NameOwnerChanged')\n\n\tdef _process_name_owner_changed(self, name, oldowner, newowner):\n\t\tif newowner != '':\n\t\t\t# so we found some new service. Check if we can do something with it.\n\t\t\tnewdeviceadded = self.scan_dbus_service(name)\n\t\t\tif newdeviceadded and self.deviceAddedCallback is not None:\n\t\t\t\tself.deviceAddedCallback(name, self.get_device_instance(name))\n\n\t\telif name in self.servicesByName:\n\t\t\t# it disappeared, we need to remove it.\n\t\t\tlogger.info(\"%s disappeared from the dbus. Removing it from our lists\" % name)\n\t\t\tservice = self.servicesByName[name]\n\t\t\tdel self.servicesById[service.id]\n\t\t\tdel self.servicesByName[name]\n\t\t\tfor watch in self.serviceWatches[name]:\n\t\t\t\twatch.remove()\n\t\t\tdel self.serviceWatches[name]\n\t\t\tself.servicesByClass[service.service_class].remove(service)\n\t\t\tif self.deviceRemovedCallback is not None:\n\t\t\t\tself.deviceRemovedCallback(name, service.deviceInstance)\n\n\tdef scan_dbus_service(self, serviceName):\n\t\ttry:\n\t\t\treturn self.scan_dbus_service_inner(serviceName)\n\t\texcept:\n\t\t\tlogger.error(\"Ignoring %s because of error while scanning:\" % (serviceName))\n\t\t\ttraceback.print_exc()\n\t\t\treturn False\n\n\t\t\t# Errors 'org.freedesktop.DBus.Error.ServiceUnknown' and\n\t\t\t# 'org.freedesktop.DBus.Error.Disconnected' seem to happen when the service\n\t\t\t# disappears while its being scanned. Which might happen, but is not really\n\t\t\t# normal either, so letting them go into the logs.\n\n\t# Scans the given dbus service to see if it contains anything interesting for us. If it does, add\n\t# it to our list of monitored D-Bus services.\n\tdef scan_dbus_service_inner(self, serviceName):\n\n\t\t# make it a normal string instead of dbus string\n\t\tserviceName = str(serviceName)\n\n\t\tpaths = self.dbusTree.get('.'.join(serviceName.split('.')[0:3]), None)\n\t\tif paths is None:\n\t\t\tlogger.debug(\"Ignoring service %s, not in the tree\" % serviceName)\n\t\t\treturn False\n\n\t\tlogger.info(\"Found: %s, scanning and storing items\" % serviceName)\n\t\tserviceId = self.dbusConn.get_name_owner(serviceName)\n\n\t\t# we should never be notified to add a D-Bus service that we already have. If this assertion\n\t\t# raises, check process_name_owner_changed, and D-Bus workings.\n\t\tassert serviceName not in self.servicesByName\n\t\tassert serviceId not in self.servicesById\n\n\t\tif serviceName == 'com.victronenergy.settings':\n\t\t\tdi = 0\n\t\telif serviceName.startswith('com.victronenergy.vecan.'):\n\t\t\tdi = 0\n\t\telse:\n\t\t\ttry:\n\t\t\t\tdi = self.dbusConn.call_blocking(serviceName,\n\t\t\t\t\t'/DeviceInstance', None, 'GetValue', '', [])\n\t\t\texcept dbus.exceptions.DBusException:\n\t\t\t\tlogger.info(\"       %s was skipped because it has no device instance\" % serviceName)\n\t\t\t\treturn False # Skip it\n\t\t\telse:\n\t\t\t\tdi = int(di)\n\n\t\tlogger.info(\"       %s has device instance %s\" % (serviceName, di))\n\t\tservice = self.make_service(serviceId, serviceName, di)\n\n\t\t# Let's try to fetch everything in one go\n\t\tvalues = {}\n\t\ttexts = {}\n\t\ttry:\n\t\t\tvalues.update(self.dbusConn.call_blocking(serviceName, '/', None, 'GetValue', '', []))\n\t\t\ttexts.update(self.dbusConn.call_blocking(serviceName, '/', None, 'GetText', '', []))\n\t\texcept:\n\t\t\tpass\n\n\t\tfor path, options in paths.items():\n\t\t\t# path will be the D-Bus path: '/Ac/ActiveIn/L1/V'\n\t\t\t# options will be a dictionary: {'code': 'V', 'whenToLog': 'onIntervalAlways'}\n\n\t\t\t# Try to obtain the value we want from our bulk fetch. If we\n\t\t\t# cannot find it there, do an individual query.\n\t\t\tvalue = values.get(path[1:], notfound)\n\t\t\tif value != notfound:\n\t\t\t\tservice.set_seen(path)\n\t\t\ttext = texts.get(path[1:], notfound)\n\t\t\tif value is notfound or text is notfound:\n\t\t\t\ttry:\n\t\t\t\t\tvalue = self.dbusConn.call_blocking(serviceName, path, None, 'GetValue', '', [])\n\t\t\t\t\tservice.set_seen(path)\n\t\t\t\t\ttext = self.dbusConn.call_blocking(serviceName, path, None, 'GetText', '', [])\n\t\t\t\texcept dbus.exceptions.DBusException as e:\n\t\t\t\t\tif e.get_dbus_name() in (\n\t\t\t\t\t\t\t'org.freedesktop.DBus.Error.ServiceUnknown',\n\t\t\t\t\t\t\t'org.freedesktop.DBus.Error.Disconnected'):\n\t\t\t\t\t\traise # This exception will be handled below\n\n\t\t\t\t\t# TODO org.freedesktop.DBus.Error.UnknownMethod really\n\t\t\t\t\t# shouldn't happen but sometimes does.\n\t\t\t\t\tlogger.debug(\"%s %s does not exist (yet)\" % (serviceName, path))\n\t\t\t\t\tvalue = None\n\t\t\t\t\ttext = None\n\n\t\t\tservice.paths[path] = self.make_monitor(service, path, unwrap_dbus_value(value), unwrap_dbus_value(text), options)\n\n\n\t\tlogger.debug(\"Finished scanning and storing items for %s\" % serviceName)\n\n\t\t# Adjust self at the end of the scan, so we don't have an incomplete set of\n\t\t# data if an exception occurs during the scan.\n\t\tself.servicesByName[serviceName] = service\n\t\tself.servicesById[serviceId] = service\n\t\tself.servicesByClass[service.service_class].append(service)\n\n\t\treturn True\n\n\tdef handler_item_changes(self, items, senderId):\n\t\tif not isinstance(items, dict):\n\t\t\treturn\n\n\t\ttry:\n\t\t\tservice = self.servicesById[senderId]\n\t\texcept KeyError:\n\t\t\t# senderId isn't there, which means it hasn't been scanned yet.\n\t\t\treturn\n\n\t\tfor path, changes in items.items():\n\t\t\ttry:\n\t\t\t\tv = unwrap_dbus_value(changes['Value'])\n\t\t\texcept (KeyError, TypeError):\n\t\t\t\tcontinue\n\n\t\t\ttry:\n\t\t\t\tt = changes['Text']\n\t\t\texcept KeyError:\n\t\t\t\tt = str(v)\n\t\t\tself._handler_value_changes(service, path, v, t)\n\n\tdef handler_value_changes(self, changes, path, senderId):\n\t\t# If this properyChange does not involve a value, our work is done.\n\t\tif 'Value' not in changes:\n\t\t\treturn\n\n\t\ttry:\n\t\t\tservice = self.servicesById[senderId]\n\t\texcept KeyError:\n\t\t\t# senderId isn't there, which means it hasn't been scanned yet.\n\t\t\treturn\n\n\t\tv = unwrap_dbus_value(changes['Value'])\n\t\t# Some services don't send Text with their PropertiesChanged events.\n\t\ttry:\n\t\t\tt = changes['Text']\n\t\texcept KeyError:\n\t\t\tt = str(v)\n\t\tself._handler_value_changes(service, path, v, t)\n\n\tdef _handler_value_changes(self, service, path, value, text):\n\t\ttry:\n\t\t\ta = service.paths[path]\n\t\texcept KeyError:\n\t\t\t# path isn't there, which means it hasn't been scanned yet.\n\t\t\treturn\n\n\t\tservice.set_seen(path)\n\n\t\t# First update our store to the new value\n\t\tif a.value == value:\n\t\t\treturn\n\n\t\ta.value = value\n\t\ta.text = text\n\n\t\t# And do the rest of the processing in on the mainloop\n\t\tif self.valueChangedCallback is not None:\n\t\t\tGLib.idle_add(exit_on_error, self._execute_value_changes, service.name, path, {\n\t\t\t\t'Value': value, 'Text': text}, a.options)\n\n\tdef _execute_value_changes(self, serviceName, objectPath, changes, options):\n\t\t# double check that the service still exists, as it might have\n\t\t# disappeared between scheduling-for and executing this function.\n\t\tif serviceName not in self.servicesByName:\n\t\t\treturn\n\n\t\tself.valueChangedCallback(serviceName, objectPath,\n\t\t\toptions, changes, self.get_device_instance(serviceName))\n\n\t# Gets the value for a certain servicename and path\n\t# The default_value is returned when:\n\t# 1. When the service doesn't exist.\n\t# 2. When the path asked for isn't being monitored.\n\t# 3. When the path exists, but has dbus-invalid, ie an empty byte array.\n\t# 4. When the path asked for is being monitored, but doesn't exist for that service.\n\tdef get_value(self, serviceName, objectPath, default_value=None):\n\t\tservice = self.servicesByName.get(serviceName, None)\n\t\tif service is None:\n\t\t\treturn default_value\n\n\t\tvalue = service.paths.get(objectPath, None)\n\t\tif value is None or value.value is None:\n\t\t\treturn default_value\n\n\t\treturn value.value\n\n\t# returns if a dbus exists now, by doing a blocking dbus call.\n\t# Typically seen will be sufficient and doesn't need access to the dbus.\n\tdef exists(self, serviceName, objectPath):\n\t\ttry:\n\t\t\tself.dbusConn.call_blocking(serviceName, objectPath, None, 'GetValue', '', [])\n\t\t\treturn True\n\t\texcept dbus.exceptions.DBusException as e:\n\t\t\treturn False\n\n\t# Returns if there ever was a successful GetValue or valueChanged event.\n\t# Unlike get_value this return True also if the actual value is invalid.\n\t#\n\t# Note: the path might no longer exists anymore, but that doesn't happen in\n\t# practice. If a service really wants to reconfigure itself typically it should\n\t# reconnect to the dbus which causes it to be rescanned and seen will be updated.\n\t# If it is really needed to know if a path still exists, use exists.\n\tdef seen(self, serviceName, objectPath):\n\t\ttry:\n\t\t\treturn self.servicesByName[serviceName].seen(objectPath)\n\t\texcept KeyError:\n\t\t\treturn False\n\n\t# Sets the value for a certain servicename and path, returns the return value of the D-Bus SetValue\n\t# method. If the underlying item does not exist (the service does not exist, or the objectPath was not\n\t# registered) the function will return -1\n\tdef set_value(self, serviceName, objectPath, value):\n\t\t# Check if the D-Bus object referenced by serviceName and objectPath is registered. There is no\n\t\t# necessity to do this, but it is in line with previous implementations which kept VeDbusItemImport\n\t\t# objects for registers items only.\n\t\tservice = self.servicesByName.get(serviceName, None)\n\t\tif service is None:\n\t\t\treturn -1\n\t\tif objectPath not in service.paths:\n\t\t\treturn -1\n\t\t# We do not catch D-Bus exceptions here, because the previous implementation did not do that either.\n\t\treturn self.dbusConn.call_blocking(serviceName, objectPath,\n\t\t\t\t   dbus_interface='com.victronenergy.BusItem',\n\t\t\t\t   method='SetValue', signature=None,\n\t\t\t\t   args=[wrap_dbus_value(value)])\n\n\t# Similar to set_value, but operates asynchronously\n\tdef set_value_async(self, serviceName, objectPath, value,\n\t\t\treply_handler=None, error_handler=None):\n\t\tservice = self.servicesByName.get(serviceName, None)\n\t\tif service is not None:\n\t\t\tif objectPath in service.paths:\n\t\t\t\tself.dbusConn.call_async(serviceName, objectPath,\n\t\t\t\t\tdbus_interface='com.victronenergy.BusItem',\n\t\t\t\t\tmethod='SetValue', signature=None,\n\t\t\t\t\targs=[wrap_dbus_value(value)],\n\t\t\t\t\treply_handler=reply_handler, error_handler=error_handler)\n\t\t\t\treturn\n\n\t\tif error_handler is not None:\n\t\t\terror_handler(TypeError('Service or path not found, '\n\t\t\t\t\t\t'service=%s, path=%s' % (serviceName, objectPath)))\n\n\t# returns a dictionary, keys are the servicenames, value the instances\n\t# optionally use the classfilter to get only a certain type of services, for\n\t# example com.victronenergy.battery.\n\tdef get_service_list(self, classfilter=None):\n\t\tif classfilter is None:\n\t\t\treturn { servicename: service.deviceInstance \\\n\t\t\t\tfor servicename, service in self.servicesByName.items() }\n\n\t\tif classfilter not in self.servicesByClass:\n\t\t\treturn {}\n\n\t\treturn { service.name: service.deviceInstance \\\n\t\t\tfor service in self.servicesByClass[classfilter] }\n\n\tdef get_device_instance(self, serviceName):\n\t\treturn self.servicesByName[serviceName].deviceInstance\n\n\tdef track_value(self, serviceName, objectPath, callback, *args, **kwargs):\n\t\t\"\"\" A DbusMonitor can watch specific service/path combos for changes\n\t\t    so that it is not fully reliant on the global handler_value_changes\n\t\t    in this class. Additional watches are deleted automatically when\n\t\t    the service disappears from dbus. \"\"\"\n\t\tcb = partial(callback, *args, **kwargs)\n\n\t\tdef root_tracker(items):\n\t\t\t# Check if objectPath in dict\n\t\t\ttry:\n\t\t\t\tv = items[objectPath]\n\t\t\t\t_v = unwrap_dbus_value(v['Value'])\n\t\t\texcept (KeyError, TypeError):\n\t\t\t\treturn # not in this dict\n\n\t\t\ttry:\n\t\t\t\tt = v['Text']\n\t\t\texcept KeyError:\n\t\t\t\tcb({'Value': _v })\n\t\t\telse:\n\t\t\t\tcb({'Value': _v, 'Text': t})\n\n\t\t# Track changes on the path, and also on root\n\t\tself.serviceWatches[serviceName].extend((\n\t\t\tself.dbusConn.add_signal_receiver(cb,\n\t\t\t\tdbus_interface='com.victronenergy.BusItem',\n\t\t\t\tsignal_name='PropertiesChanged',\n\t\t\t\tpath=objectPath, bus_name=serviceName),\n\t\t\tself.dbusConn.add_signal_receiver(root_tracker,\n\t\t\t\tdbus_interface='com.victronenergy.BusItem',\n\t\t\t\tsignal_name='ItemsChanged',\n\t\t\t\tpath=\"/\", bus_name=serviceName),\n\t\t))\n\n\n# ====== ALL CODE BELOW THIS LINE IS PURELY FOR DEVELOPING THIS CLASS ======\n\n# Example function that can be used as a starting point to use this code\ndef value_changed_on_dbus(dbusServiceName, dbusPath, dict, changes, deviceInstance):\n\tlogger.debug(\"0 ----------------\")\n\tlogger.debug(\"1 %s%s changed\" % (dbusServiceName, dbusPath))\n\tlogger.debug(\"2 vrm dict     : %s\" % dict)\n\tlogger.debug(\"3 changes-text: %s\" % changes['Text'])\n\tlogger.debug(\"4 changes-value: %s\" % changes['Value'])\n\tlogger.debug(\"5 deviceInstance: %s\" % deviceInstance)\n\tlogger.debug(\"6 - end\")\n\n\ndef nameownerchange(a, b):\n\t# used to find memory leaks in dbusmonitor and VeDbusItemImport\n\timport gc\n\tgc.collect()\n\tobjects = gc.get_objects()\n\tprint (len([o for o in objects if type(o).__name__ == 'VeDbusItemImport']))\n\tprint (len([o for o in objects if type(o).__name__ == 'SignalMatch']))\n\tprint (len(objects))\n\n\ndef print_values(dbusmonitor):\n\ta = dbusmonitor.get_value('wrongservice', '/DbusInvalid', default_value=1000)\n\tb = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/NotInTheMonitorList', default_value=1000)\n\tc = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/DbusInvalid', default_value=1000)\n\td = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/NonExistingButMonitored', default_value=1000)\n\n\tprint (\"All should be 1000: Wrong Service: %s, NotInTheMonitorList: %s, DbusInvalid: %s, NonExistingButMonitored: %s\" % (a, b, c, d))\n\treturn True\n\n# We have a mainloop, but that is just for developing this code. Normally above class & code is used from\n# some other class, such as vrmLogger or the pubsub Implementation.\ndef main():\n\t# Init logging\n\tlogging.basicConfig(level=logging.DEBUG)\n\tlogger.info(__file__ + \" is starting up\")\n\n\t# Have a mainloop, so we can send/receive asynchronous calls to and from dbus\n\tDBusGMainLoop(set_as_default=True)\n\n\timport os\n\timport sys\n\tsys.path.insert(1, os.path.join(os.path.dirname(__file__), '../../'))\n\n\tdummy = {'code': None, 'whenToLog': 'configChange', 'accessLevel': None}\n\tmonitorlist = {'com.victronenergy.dummyservice': {\n\t\t\t\t'/Connected': dummy,\n\t\t\t\t'/ProductName': dummy,\n\t\t\t\t'/Mgmt/Connection': dummy,\n\t\t\t\t'/Dc/0/Voltage': dummy,\n\t\t\t\t'/Dc/0/Current': dummy,\n\t\t\t\t'/Dc/0/Temperature': dummy,\n\t\t\t\t'/Load/I': dummy,\n\t\t\t\t'/FirmwareVersion': dummy,\n\t\t\t\t'/DbusInvalid': dummy,\n\t\t\t\t'/NonExistingButMonitored': dummy}}\n\n\td = DbusMonitor(monitorlist, value_changed_on_dbus,\n\t\tdeviceAddedCallback=nameownerchange, deviceRemovedCallback=nameownerchange)\n\n\tGLib.timeout_add(1000, print_values, d)\n\n\t# Start and run the mainloop\n\tlogger.info(\"Starting mainloop, responding on only events\")\n\tmainloop = GLib.MainLoop()\n\tmainloop.run()\n\nif __name__ == \"__main__\":\n\tmain()\n"
  },
  {
    "path": "velib_python/velib_python/v3.34/oldestVersion",
    "content": "v3.10\n"
  },
  {
    "path": "velib_python/velib_python/v3.34/settingsdevice.py",
    "content": "import dbus\nimport logging\nimport time\nfrom functools import partial\n\n# Local imports\nfrom vedbus import VeDbusItemImport\n\n## Indexes for the setting dictonary.\nPATH = 0\nVALUE = 1\nMINIMUM = 2\nMAXIMUM = 3\nSILENT = 4\n\n## The Settings Device class.\n# Used by python programs, such as the vrm-logger, to read and write settings they\n# need to store on disk. And since these settings might be changed from a different\n# source, such as the GUI, the program can pass an eventCallback that will be called\n# as soon as some setting is changed.\n#\n# The settings are stored in flash via the com.victronenergy.settings service on dbus.\n# See https://github.com/victronenergy/localsettings for more info.\n#\n# If there are settings in de supportSettings list which are not yet on the dbus, \n# and therefore not yet in the xml file, they will be added through the dbus-addSetting\n# interface of com.victronenergy.settings.\nclass SettingsDevice(object):\n\t## The constructor processes the tree of dbus-items.\n\t# @param bus the system-dbus object\n\t# @param name the dbus-service-name of the settings dbus service, 'com.victronenergy.settings'\n\t# @param supportedSettings dictionary with all setting-names, and their defaultvalue, min, max and whether\n\t# the setting is silent. The 'silent' entry is optional. If set to true, no changes in the setting will\n\t# be logged by localsettings.\n\t# @param eventCallback function that will be called on changes on any of these settings\n\t# @param timeout Maximum interval to wait for localsettings. An exception is thrown at the end of the\n\t# interval if the localsettings D-Bus service has not appeared yet.\n\tdef __init__(self, bus, supportedSettings, eventCallback, name='com.victronenergy.settings', timeout=0):\n\t\tlogging.debug(\"===== Settings device init starting... =====\")\n\t\tself._bus = bus\n\t\tself._dbus_name = name\n\t\tself._eventCallback = eventCallback\n\t\tself._values = {} # stored the values, used to pass the old value along on a setting change\n\t\tself._settings = {}\n\n\t\tcount = 0\n\t\twhile True:\n\t\t\tif 'com.victronenergy.settings' in self._bus.list_names():\n\t\t\t\tbreak\n\t\t\tif count == timeout:\n\t\t\t\traise Exception(\"The settings service com.victronenergy.settings does not exist!\")\n\t\t\tcount += 1\n\t\t\tlogging.info('waiting for settings')\n\t\t\ttime.sleep(1)\n\n\t\t# Add the items.\n\t\tself.addSettings(supportedSettings)\n\n\t\tlogging.debug(\"===== Settings device init finished =====\")\n\n\tdef addSettings(self, settings):\n\t\tfor setting, options in settings.items():\n\t\t\tsilent = len(options) > SILENT and options[SILENT]\n\t\t\tbusitem = self.addSetting(options[PATH], options[VALUE],\n\t\t\t\toptions[MINIMUM], options[MAXIMUM], silent, callback=partial(self.handleChangedSetting, setting))\n\t\t\tself._settings[setting] = busitem\n\t\t\tself._values[setting] = busitem.get_value()\n\n\tdef addSetting(self, path, value, _min, _max, silent=False, callback=None):\n\t\tbusitem = VeDbusItemImport(self._bus, self._dbus_name, path, callback)\n\t\tif busitem.exists and (value, _min, _max, silent) == busitem._proxy.GetAttributes():\n\t\t\tlogging.debug(\"Setting %s found\" % path)\n\t\telse:\n\t\t\tlogging.info(\"Setting %s does not exist yet or must be adjusted\" % path)\n\n\t\t\t# Prepare to add the setting. Most dbus types extend the python\n\t\t\t# type so it is only necessary to additionally test for Int64.\n\t\t\tif isinstance(value, (int, dbus.Int64)):\n\t\t\t\titemType = 'i'\n\t\t\telif isinstance(value, float):\n\t\t\t\titemType = 'f'\n\t\t\telse:\n\t\t\t\titemType = 's'\n\n\t\t\t# Add the setting\n\t\t\t# TODO, make an object that inherits VeDbusItemImport, and complete the D-Bus settingsitem interface\n\t\t\tsettings_item = VeDbusItemImport(self._bus, self._dbus_name, '/Settings', createsignal=False)\n\t\t\tsetting_path = path.replace('/Settings/', '', 1)\n\t\t\tif silent:\n\t\t\t\tsettings_item._proxy.AddSilentSetting('', setting_path, value, itemType, _min, _max)\n\t\t\telse:\n\t\t\t\tsettings_item._proxy.AddSetting('', setting_path, value, itemType, _min, _max)\n\n\t\t\tbusitem = VeDbusItemImport(self._bus, self._dbus_name, path, callback)\n\n\t\treturn busitem\n\n\tdef handleChangedSetting(self, setting, servicename, path, changes):\n\t\toldvalue = self._values[setting] if setting in self._values else None\n\t\tself._values[setting] = changes['Value']\n\n\t\tif self._eventCallback is None:\n\t\t\treturn\n\n\t\tself._eventCallback(setting, oldvalue, changes['Value'])\n\n\tdef setDefault(self, path):\n                item = VeDbusItemImport(self._bus, self._dbus_name, path, createsignal=False)\n                item.set_default()\n\n\tdef __getitem__(self, setting):\n\t\treturn self._settings[setting].get_value()\n\n\tdef __setitem__(self, setting, newvalue):\n\t\tresult = self._settings[setting].set_value(newvalue)\n\t\tif result != 0:\n\t\t\t# Trying to make some false change to our own settings? How dumb!\n\t\t\tassert False\n"
  },
  {
    "path": "velib_python/velib_python/v3.34/ve_utils.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\nimport sys\nfrom traceback import print_exc\nfrom os import _exit as os_exit\nfrom os import statvfs\nfrom subprocess import check_output, CalledProcessError\nimport logging\nimport dbus\nlogger = logging.getLogger(__name__)\n\nVEDBUS_INVALID = dbus.Array([], signature=dbus.Signature('i'), variant_level=1)\n\nclass NoVrmPortalIdError(Exception):\n\tpass\n\n# Use this function to make sure the code quits on an unexpected exception. Make sure to use it\n# when using GLib.idle_add and also GLib.timeout_add.\n# Without this, the code will just keep running, since GLib does not stop the mainloop on an\n# exception.\n# Example: GLib.idle_add(exit_on_error, myfunc, arg1, arg2)\ndef exit_on_error(func, *args, **kwargs):\n\ttry:\n\t\treturn func(*args, **kwargs)\n\texcept:\n\t\ttry:\n\t\t\tprint ('exit_on_error: there was an exception. Printing stacktrace will be tried and then exit')\n\t\t\tprint_exc()\n\t\texcept:\n\t\t\tpass\n\n\t\t# sys.exit() is not used, since that throws an exception, which does not lead to a program\n\t\t# halt when used in a dbus callback, see connection.py in the Python/Dbus libraries, line 230.\n\t\tos_exit(1)\n\n\n__vrm_portal_id = None\ndef get_vrm_portal_id():\n\t# The original definition of the VRM Portal ID is that it is the mac\n\t# address of the onboard- ethernet port (eth0), stripped from its colons\n\t# (:) and lower case. This may however differ between platforms. On Venus\n\t# the task is therefore deferred to /sbin/get-unique-id so that a\n\t# platform specific method can be easily defined.\n\t#\n\t# If /sbin/get-unique-id does not exist, then use the ethernet address\n\t# of eth0. This also handles the case where velib_python is used as a\n\t# package install on a Raspberry Pi.\n\t#\n\t# On a Linux host where the network interface may not be eth0, you can set\n\t# the VRM_IFACE environment variable to the correct name.\n\n\tglobal __vrm_portal_id\n\n\tif __vrm_portal_id:\n\t\treturn __vrm_portal_id\n\n\tportal_id = None\n\n\t# First try the method that works if we don't have a data partition. This\n\t# will fail when the current user is not root.\n\ttry:\n\t\tportal_id = check_output(\"/sbin/get-unique-id\").decode(\"utf-8\", \"ignore\").strip()\n\t\tif not portal_id:\n\t\t\traise NoVrmPortalIdError(\"get-unique-id returned blank\")\n\t\t__vrm_portal_id = portal_id\n\t\treturn portal_id\n\texcept CalledProcessError:\n\t\t# get-unique-id returned non-zero\n\t\traise NoVrmPortalIdError(\"get-unique-id returned non-zero\")\n\texcept OSError:\n\t\t# File doesn't exist, use fallback\n\t\tpass\n\n\t# Fall back to getting our id using a syscall. Assume we are on linux.\n\t# Allow the user to override what interface is used using an environment\n\t# variable.\n\timport fcntl, socket, struct, os\n\n\tiface = os.environ.get('VRM_IFACE', 'eth0').encode('ascii')\n\ts = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)\n\ttry:\n\t\tinfo = fcntl.ioctl(s.fileno(), 0x8927,  struct.pack('256s', iface[:15]))\n\texcept IOError:\n\t\traise NoVrmPortalIdError(\"ioctl failed for eth0\")\n\n\t__vrm_portal_id = info[18:24].hex()\n\treturn __vrm_portal_id\n\n\n# See VE.Can registers - public.docx for definition of this conversion\ndef convert_vreg_version_to_readable(version):\n\tdef str_to_arr(x, length):\n\t\ta = []\n\t\tfor i in range(0, len(x), length):\n\t\t\ta.append(x[i:i+length])\n\t\treturn a\n\n\tx = \"%x\" % version\n\tx = x.upper()\n\n\tif len(x) == 5 or len(x) == 3 or len(x) == 1:\n\t\tx = '0' + x\n\n\ta = str_to_arr(x, 2);\n\n\t# remove the first 00 if there are three bytes and it is 00\n\tif len(a) == 3 and a[0] == '00':\n\t\ta.remove(0);\n\n\t# if we have two or three bytes now, and the first character is a 0, remove it\n\tif len(a) >= 2 and a[0][0:1] == '0':\n\t\ta[0] = a[0][1];\n\n\tresult = ''\n\tfor item in a:\n\t\tresult += ('.' if result != '' else '') + item\n\n\n\tresult = 'v' + result\n\n\treturn result\n\n\ndef get_free_space(path):\n\tresult = -1\n\n\ttry:\n\t\ts = statvfs(path)\n\t\tresult = s.f_frsize * s.f_bavail     # Number of free bytes that ordinary users\n\texcept Exception as ex:\n\t\tlogger.info(\"Error while retrieving free space for path %s: %s\" % (path, ex))\n\n\treturn result\n\n\ndef _get_sysfs_machine_name():\n\ttry:\n\t\twith open('/sys/firmware/devicetree/base/model', 'r') as f:\n\t\t\treturn f.read().rstrip('\\x00')\n\texcept IOError:\n\t\tpass\n\n\treturn None\n\n# Returns None if it cannot find a machine name. Otherwise returns the string\n# containing the name\ndef get_machine_name():\n\t# First try calling the venus utility script\n\ttry:\n\t\treturn check_output(\"/usr/bin/product-name\").strip().decode('UTF-8')\n\texcept (CalledProcessError, OSError):\n\t\tpass\n\n\t# Fall back to sysfs\n\tname = _get_sysfs_machine_name()\n\tif name is not None:\n\t\treturn name\n\n\t# Fall back to venus build machine name\n\ttry:\n\t\twith open('/etc/venus/machine', 'r', encoding='UTF-8') as f:\n\t\t\treturn f.read().strip()\n\texcept IOError:\n\t\tpass\n\n\treturn None\n\n\ndef get_product_id():\n\t\"\"\" Find the machine ID and return it. \"\"\"\n\n\t# First try calling the venus utility script\n\ttry:\n\t\treturn check_output(\"/usr/bin/product-id\").strip().decode('UTF-8')\n\texcept (CalledProcessError, OSError):\n\t\tpass\n\n\t# Fall back machine name mechanism\n\tname = _get_sysfs_machine_name()\n\treturn {\n\t\t'Color Control GX': 'C001',\n\t\t'Venus GX': 'C002',\n\t\t'Octo GX': 'C006',\n\t\t'EasySolar-II': 'C007',\n\t\t'MultiPlus-II': 'C008',\n\t\t'Maxi GX': 'C009',\n\t\t'Cerbo GX': 'C00A'\n\t}.get(name, 'C003') # C003 is Generic\n\n\n# Returns False if it cannot open the file. Otherwise returns its rstripped contents\ndef read_file(path):\n\tcontent = False\n\n\ttry:\n\t\twith open(path, 'r') as f:\n\t\t\tcontent = f.read().rstrip()\n\texcept Exception as ex:\n\t\tlogger.debug(\"Error while reading %s: %s\" % (path, ex))\n\n\treturn content\n\n\ndef wrap_dbus_value(value):\n\tif value is None:\n\t\treturn VEDBUS_INVALID\n\tif isinstance(value, float):\n\t\treturn dbus.Double(value, variant_level=1)\n\tif isinstance(value, bool):\n\t\treturn dbus.Boolean(value, variant_level=1)\n\tif isinstance(value, int):\n\t\ttry:\n\t\t\treturn dbus.Int32(value, variant_level=1)\n\t\texcept OverflowError:\n\t\t\treturn dbus.Int64(value, variant_level=1)\n\tif isinstance(value, str):\n\t\treturn dbus.String(value, variant_level=1)\n\tif isinstance(value, list):\n\t\tif len(value) == 0:\n\t\t\t# If the list is empty we cannot infer the type of the contents. So assume unsigned integer.\n\t\t\t# A (signed) integer is dangerous, because an empty list of signed integers is used to encode\n\t\t\t# an invalid value.\n\t\t\treturn dbus.Array([], signature=dbus.Signature('u'), variant_level=1)\n\t\treturn dbus.Array([wrap_dbus_value(x) for x in value], variant_level=1)\n\tif isinstance(value, dict):\n\t\t# Wrapping the keys of the dictionary causes D-Bus errors like:\n\t\t# 'arguments to dbus_message_iter_open_container() were incorrect,\n\t\t# assertion \"(type == DBUS_TYPE_ARRAY && contained_signature &&\n\t\t# *contained_signature == DBUS_DICT_ENTRY_BEGIN_CHAR) || (contained_signature == NULL ||\n\t\t# _dbus_check_is_valid_signature (contained_signature))\" failed in file ...'\n\t\treturn dbus.Dictionary({(k, wrap_dbus_value(v)) for k, v in value.items()}, variant_level=1)\n\treturn value\n\n\ndbus_int_types = (dbus.Int32, dbus.UInt32, dbus.Byte, dbus.Int16, dbus.UInt16, dbus.UInt32, dbus.Int64, dbus.UInt64)\n\n\ndef unwrap_dbus_value(val):\n\t\"\"\"Converts D-Bus values back to the original type. For example if val is of type DBus.Double,\n\ta float will be returned.\"\"\"\n\tif isinstance(val, dbus_int_types):\n\t\treturn int(val)\n\tif isinstance(val, dbus.Double):\n\t\treturn float(val)\n\tif isinstance(val, dbus.Array):\n\t\tv = [unwrap_dbus_value(x) for x in val]\n\t\treturn None if len(v) == 0 else v\n\tif isinstance(val, (dbus.Signature, dbus.String)):\n\t\treturn str(val)\n\t# Python has no byte type, so we convert to an integer.\n\tif isinstance(val, dbus.Byte):\n\t\treturn int(val)\n\tif isinstance(val, dbus.ByteArray):\n\t\treturn \"\".join([bytes(x) for x in val])\n\tif isinstance(val, (list, tuple)):\n\t\treturn [unwrap_dbus_value(x) for x in val]\n\tif isinstance(val, (dbus.Dictionary, dict)):\n\t\t# Do not unwrap the keys, see comment in wrap_dbus_value\n\t\treturn dict([(x, unwrap_dbus_value(y)) for x, y in val.items()])\n\tif isinstance(val, dbus.Boolean):\n\t\treturn bool(val)\n\treturn val\n"
  },
  {
    "path": "velib_python/velib_python/v3.34/vedbus.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\nimport dbus.service\nimport logging\nimport traceback\nimport os\nimport weakref\nfrom collections import defaultdict\nfrom ve_utils import wrap_dbus_value, unwrap_dbus_value\n\n# vedbus contains three classes:\n# VeDbusItemImport -> use this to read data from the dbus, ie import\n# VeDbusItemExport -> use this to export data to the dbus (one value)\n# VeDbusService -> use that to create a service and export several values to the dbus\n\n# Code for VeDbusItemImport is copied from busitem.py and thereafter modified.\n# All projects that used busitem.py need to migrate to this package. And some\n# projects used to define there own equivalent of VeDbusItemExport. Better to\n# use VeDbusItemExport, or even better the VeDbusService class that does it all for you.\n\n# TODOS\n# 1 check for datatypes, it works now, but not sure if all is compliant with\n#\tcom.victronenergy.BusItem interface definition. See also the files in\n#\ttests_and_examples. And see 'if type(v) == dbus.Byte:' on line 102. Perhaps\n#\tsomething similar should also be done in VeDbusBusItemExport?\n# 2 Shouldn't VeDbusBusItemExport inherit dbus.service.Object?\n# 7 Make hard rules for services exporting data to the D-Bus, in order to make tracking\n#   changes possible. Does everybody first invalidate its data before leaving the bus?\n#   And what about before taking one object away from the bus, instead of taking the\n#   whole service offline?\n#   They should! And after taking one value away, do we need to know that someone left\n#   the bus? Or we just keep that value in invalidated for ever? Result is that we can't\n#   see the difference anymore between an invalidated value and a value that was first on\n#   the bus and later not anymore. See comments above VeDbusItemImport as well.\n# 9 there are probably more todos in the code below.\n\n# Some thoughts with regards to the data types:\n#\n#   Text from: http://dbus.freedesktop.org/doc/dbus-python/doc/tutorial.html#data-types\n#   ---\n#   Variants are represented by setting the variant_level keyword argument in the\n#   constructor of any D-Bus data type to a value greater than 0 (variant_level 1\n#   means a variant containing some other data type, variant_level 2 means a variant\n#   containing a variant containing some other data type, and so on). If a non-variant\n#   is passed as an argument but introspection indicates that a variant is expected,\n#   it'll automatically be wrapped in a variant.\n#   ---\n#\n#   Also the different dbus datatypes, such as dbus.Int32, and dbus.UInt32 are a subclass\n#   of Python int. dbus.String is a subclass of Python standard class unicode, etcetera\n#\n#   So all together that explains why we don't need to explicitly convert back and forth\n#   between the dbus datatypes and the standard python datatypes. Note that all datatypes\n#   in python are objects. Even an int is an object.\n\n#   The signature of a variant is 'v'.\n\n# Export ourselves as a D-Bus service.\nclass VeDbusService(object):\n\tdef __init__(self, servicename, bus=None):\n\t\t# dict containing the VeDbusItemExport objects, with their path as the key.\n\t\tself._dbusobjects = {}\n\t\tself._dbusnodes = {}\n\t\tself._ratelimiters = []\n\t\tself._dbusname = None\n\n\t\t# dict containing the onchange callbacks, for each object. Object path is the key\n\t\tself._onchangecallbacks = {}\n\n\t\t# Connect to session bus whenever present, else use the system bus\n\t\tself._dbusconn = bus or (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else dbus.SystemBus())\n\n\t\t# make the dbus connection available to outside, could make this a true property instead, but ach..\n\t\tself.dbusconn = self._dbusconn\n\n\t\t# Register ourselves on the dbus, trigger an error if already in use (do_not_queue)\n\t\tself._dbusname = dbus.service.BusName(servicename, self._dbusconn, do_not_queue=True)\n\n\t\t# Add the root item that will return all items as a tree\n\t\tself._dbusnodes['/'] = VeDbusRootExport(self._dbusconn, '/', self)\n\n\t\tlogging.info(\"registered ourselves on D-Bus as %s\" % servicename)\n\n\t# To force immediate deregistering of this dbus service and all its object paths, explicitly\n\t# call __del__().\n\tdef __del__(self):\n\t\tfor node in list(self._dbusnodes.values()):\n\t\t\tnode.__del__()\n\t\tself._dbusnodes.clear()\n\t\tfor item in list(self._dbusobjects.values()):\n\t\t\titem.__del__()\n\t\tself._dbusobjects.clear()\n\t\tif self._dbusname:\n\t\t\tself._dbusname.__del__()  # Forces call to self._bus.release_name(self._name), see source code\n\t\tself._dbusname = None\n\n\t# @param callbackonchange\tfunction that will be called when this value is changed. First parameter will\n\t#\t\t\t\t\t\t\tbe the path of the object, second the new value. This callback should return\n\t#\t\t\t\t\t\t\tTrue to accept the change, False to reject it.\n\tdef add_path(self, path, value, description=\"\", writeable=False,\n\t\t\t\t\tonchangecallback=None, gettextcallback=None, valuetype=None):\n\n\t\tif onchangecallback is not None:\n\t\t\tself._onchangecallbacks[path] = onchangecallback\n\n\t\titem = VeDbusItemExport(\n\t\t\t\tself._dbusconn, path, value, description, writeable,\n\t\t\t\tself._value_changed, gettextcallback, deletecallback=self._item_deleted, valuetype=valuetype)\n\n\t\tspl = path.split('/')\n\t\tfor i in range(2, len(spl)):\n\t\t\tsubPath = '/'.join(spl[:i])\n\t\t\tif subPath not in self._dbusnodes and subPath not in self._dbusobjects:\n\t\t\t\tself._dbusnodes[subPath] = VeDbusTreeExport(self._dbusconn, subPath, self)\n\t\tself._dbusobjects[path] = item\n\t\tlogging.debug('added %s with start value %s. Writeable is %s' % (path, value, writeable))\n\n\t# Add the mandatory paths, as per victron dbus api doc\n\tdef add_mandatory_paths(self, processname, processversion, connection,\n\t\t\tdeviceinstance, productid, productname, firmwareversion, hardwareversion, connected):\n\t\tself.add_path('/Mgmt/ProcessName', processname)\n\t\tself.add_path('/Mgmt/ProcessVersion', processversion)\n\t\tself.add_path('/Mgmt/Connection', connection)\n\n\t\t# Create rest of the mandatory objects\n\t\tself.add_path('/DeviceInstance', deviceinstance)\n\t\tself.add_path('/ProductId', productid)\n\t\tself.add_path('/ProductName', productname)\n\t\tself.add_path('/FirmwareVersion', firmwareversion)\n\t\tself.add_path('/HardwareVersion', hardwareversion)\n\t\tself.add_path('/Connected', connected)\n\n\t# Callback function that is called from the VeDbusItemExport objects when a value changes. This function\n\t# maps the change-request to the onchangecallback given to us for this specific path.\n\tdef _value_changed(self, path, newvalue):\n\t\tif path not in self._onchangecallbacks:\n\t\t\treturn True\n\n\t\treturn self._onchangecallbacks[path](path, newvalue)\n\n\tdef _item_deleted(self, path):\n\t\tself._dbusobjects.pop(path)\n\t\tfor np in list(self._dbusnodes.keys()):\n\t\t\tif np != '/':\n\t\t\t\tfor ip in self._dbusobjects:\n\t\t\t\t\tif ip.startswith(np + '/'):\n\t\t\t\t\t\tbreak\n\t\t\t\telse:\n\t\t\t\t\tself._dbusnodes[np].__del__()\n\t\t\t\t\tself._dbusnodes.pop(np)\n\n\tdef __getitem__(self, path):\n\t\treturn self._dbusobjects[path].local_get_value()\n\n\tdef __setitem__(self, path, newvalue):\n\t\tself._dbusobjects[path].local_set_value(newvalue)\n\n\tdef __delitem__(self, path):\n\t\tself._dbusobjects[path].__del__()  # Invalidates and then removes the object path\n\t\tassert path not in self._dbusobjects\n\n\tdef __contains__(self, path):\n\t\treturn path in self._dbusobjects\n\n\tdef __enter__(self):\n\t\tl = ServiceContext(self)\n\t\tself._ratelimiters.append(l)\n\t\treturn l\n\n\tdef __exit__(self, *exc):\n\t\t# pop off the top one and flush it. If with statements are nested\n\t\t# then each exit flushes its own part.\n\t\tif self._ratelimiters:\n\t\t\tself._ratelimiters.pop().flush()\n\nclass ServiceContext(object):\n\tdef __init__(self, parent):\n\t\tself.parent = parent\n\t\tself.changes = {}\n\n\tdef __getitem__(self, path):\n\t\treturn self.parent[path]\n\n\tdef __setitem__(self, path, newvalue):\n\t\tc = self.parent._dbusobjects[path]._local_set_value(newvalue)\n\t\tif c is not None:\n\t\t\tself.changes[path] = c\n\n\tdef flush(self):\n\t\tif self.changes:\n\t\t\tself.parent._dbusnodes['/'].ItemsChanged(self.changes)\n\nclass TrackerDict(defaultdict):\n\t\"\"\" Same as defaultdict, but passes the key to default_factory. \"\"\"\n\tdef __missing__(self, key):\n\t\tself[key] = x = self.default_factory(key)\n\t\treturn x\n\nclass VeDbusRootTracker(object):\n\t\"\"\" This tracks the root of a dbus path and listens for PropertiesChanged\n\t    signals. When a signal arrives, parse it and unpack the key/value changes\n\t    into traditional events, then pass it to the original eventCallback\n\t    method. \"\"\"\n\tdef __init__(self, bus, serviceName):\n\t\tself.importers = defaultdict(weakref.WeakSet)\n\t\tself.serviceName = serviceName\n\t\tself._match = bus.get_object(serviceName, '/', introspect=False).connect_to_signal(\n\t\t\t\"ItemsChanged\", weak_functor(self._items_changed_handler))\n\n\tdef __del__(self):\n\t\tself._match.remove()\n\t\tself._match = None\n\n\tdef add(self, i):\n\t\tself.importers[i.path].add(i)\n\n\tdef _items_changed_handler(self, items):\n\t\tif not isinstance(items, dict):\n\t\t\treturn\n\n\t\tfor path, changes in items.items():\n\t\t\ttry:\n\t\t\t\tv = changes['Value']\n\t\t\texcept KeyError:\n\t\t\t\tcontinue\n\n\t\t\ttry:\n\t\t\t\tt = changes['Text']\n\t\t\texcept KeyError:\n\t\t\t\tt = str(unwrap_dbus_value(v))\n\n\t\t\tfor i in self.importers.get(path, ()):\n\t\t\t\ti._properties_changed_handler({'Value': v, 'Text': t})\n\n\"\"\"\nImporting basics:\n\t- If when we power up, the D-Bus service does not exist, or it does exist and the path does not\n\t  yet exist, still subscribe to a signal: as soon as it comes online it will send a signal with its\n\t  initial value, which VeDbusItemImport will receive and use to update local cache. And, when set,\n\t  call the eventCallback.\n\t- If when we power up, save it\n\t- When using get_value, know that there is no difference between services (or object paths) that don't\n\t  exist and paths that are invalid (= empty array, see above). Both will return None. In case you do\n\t  really want to know ifa path exists or not, use the exists property.\n\t- When a D-Bus service leaves the D-Bus, it will first invalidate all its values, and send signals\n\t  with that update, and only then leave the D-Bus. (or do we need to subscribe to the NameOwnerChanged-\n\t  signal!?!) To be discussed and make sure. Not really urgent, since all existing code that uses this\n\t  class already subscribes to the NameOwnerChanged signal, and subsequently removes instances of this\n\t  class.\n\nRead when using this class:\nNote that when a service leaves that D-Bus without invalidating all its exported objects first, for\nexample because it is killed, VeDbusItemImport doesn't have a clue. So when using VeDbusItemImport,\nmake sure to also subscribe to the NamerOwnerChanged signal on bus-level. Or just use dbusmonitor,\nbecause that takes care of all of that for you.\n\"\"\"\nclass VeDbusItemImport(object):\n\tdef __new__(cls, bus, serviceName, path, eventCallback=None, createsignal=True):\n\t\tinstance = object.__new__(cls)\n\n\t\t# If signal tracking should be done, also add to root tracker\n\t\tif createsignal:\n\t\t\tif \"_roots\" not in cls.__dict__:\n\t\t\t\tcls._roots = TrackerDict(lambda k: VeDbusRootTracker(bus, k))\n\n\t\treturn instance\n\n\t## Constructor\n\t# @param bus\t\t\tthe bus-object (SESSION or SYSTEM).\n\t# @param serviceName\tthe dbus-service-name (string), for example 'com.victronenergy.battery.ttyO1'\n\t# @param path\t\t\tthe object-path, for example '/Dc/V'\n\t# @param eventCallback\tfunction that you want to be called on a value change\n\t# @param createSignal   only set this to False if you use this function to one time read a value. When\n\t#\t\t\t\t\t\tleaving it to True, make sure to also subscribe to the NameOwnerChanged signal\n\t#\t\t\t\t\t\telsewhere. See also note some 15 lines up.\n\tdef __init__(self, bus, serviceName, path, eventCallback=None, createsignal=True):\n\t\t# TODO: is it necessary to store _serviceName and _path? Isn't it\n\t\t# stored in the bus_getobjectsomewhere?\n\t\tself._serviceName = serviceName\n\t\tself._path = path\n\t\tself._match = None\n\t\t# TODO: _proxy is being used in settingsdevice.py, make a getter for that\n\t\tself._proxy = bus.get_object(serviceName, path, introspect=False)\n\t\tself.eventCallback = eventCallback\n\n\t\tassert eventCallback is None or createsignal == True\n\t\tif createsignal:\n\t\t\tself._match = self._proxy.connect_to_signal(\n\t\t\t\t\"PropertiesChanged\", weak_functor(self._properties_changed_handler))\n\t\t\tself._roots[serviceName].add(self)\n\n\t\t# store the current value in _cachedvalue. When it doesn't exists set _cachedvalue to\n\t\t# None, same as when a value is invalid\n\t\tself._cachedvalue = None\n\t\ttry:\n\t\t\tv = self._proxy.GetValue()\n\t\texcept dbus.exceptions.DBusException:\n\t\t\tpass\n\t\telse:\n\t\t\tself._cachedvalue = unwrap_dbus_value(v)\n\n\tdef __del__(self):\n\t\tif self._match is not None:\n\t\t\tself._match.remove()\n\t\t\tself._match = None\n\t\tself._proxy = None\n\n\tdef _refreshcachedvalue(self):\n\t\tself._cachedvalue = unwrap_dbus_value(self._proxy.GetValue())\n\n\t## Returns the path as a string, for example '/AC/L1/V'\n\t@property\n\tdef path(self):\n\t\treturn self._path\n\n\t## Returns the dbus service name as a string, for example com.victronenergy.vebus.ttyO1\n\t@property\n\tdef serviceName(self):\n\t\treturn self._serviceName\n\n\t## Returns the value of the dbus-item.\n\t# the type will be a dbus variant, for example dbus.Int32(0, variant_level=1)\n\t# this is not a property to keep the name consistant with the com.victronenergy.busitem interface\n\t# returns None when the property is invalid\n\tdef get_value(self):\n\t\treturn self._cachedvalue\n\n\t## Writes a new value to the dbus-item\n\tdef set_value(self, newvalue):\n\t\tr = self._proxy.SetValue(wrap_dbus_value(newvalue))\n\n\t\t# instead of just saving the value, go to the dbus and get it. So we have the right type etc.\n\t\tif r == 0:\n\t\t\tself._refreshcachedvalue()\n\n\t\treturn r\n\n\t## Resets the item to its default value\n\tdef set_default(self):\n\t\tself._proxy.SetDefault()\n\t\tself._refreshcachedvalue()\n\n\t## Returns the text representation of the value.\n\t# For example when the value is an enum/int GetText might return the string\n\t# belonging to that enum value. Another example, for a voltage, GetValue\n\t# would return a float, 12.0Volt, and GetText could return 12 VDC.\n\t#\n\t# Note that this depends on how the dbus-producer has implemented this.\n\tdef get_text(self):\n\t\treturn self._proxy.GetText()\n\n\t## Returns true of object path exists, and false if it doesn't\n\t@property\n\tdef exists(self):\n\t\t# TODO: do some real check instead of this crazy thing.\n\t\tr = False\n\t\ttry:\n\t\t\tr = self._proxy.GetValue()\n\t\t\tr = True\n\t\texcept dbus.exceptions.DBusException:\n\t\t\tpass\n\n\t\treturn r\n\n\t## callback for the trigger-event.\n\t# @param eventCallback the event-callback-function.\n\t@property\n\tdef eventCallback(self):\n\t\treturn self._eventCallback\n\n\t@eventCallback.setter\n\tdef eventCallback(self, eventCallback):\n\t\tself._eventCallback = eventCallback\n\n\t## Is called when the value of the imported bus-item changes.\n\t# Stores the new value in our local cache, and calls the eventCallback, if set.\n\tdef _properties_changed_handler(self, changes):\n\t\tif \"Value\" in changes:\n\t\t\tchanges['Value'] = unwrap_dbus_value(changes['Value'])\n\t\t\tself._cachedvalue = changes['Value']\n\t\t\tif self._eventCallback:\n\t\t\t\t# The reason behind this try/except is to prevent errors silently ending up the an error\n\t\t\t\t# handler in the dbus code.\n\t\t\t\ttry:\n\t\t\t\t\tself._eventCallback(self._serviceName, self._path, changes)\n\t\t\t\texcept:\n\t\t\t\t\ttraceback.print_exc()\n\t\t\t\t\tos._exit(1)  # sys.exit() is not used, since that also throws an exception\n\n\nclass VeDbusTreeExport(dbus.service.Object):\n\tdef __init__(self, bus, objectPath, service):\n\t\tdbus.service.Object.__init__(self, bus, objectPath)\n\t\tself._service = service\n\t\tlogging.debug(\"VeDbusTreeExport %s has been created\" % objectPath)\n\n\tdef __del__(self):\n\t\t# self._get_path() will raise an exception when retrieved after the call to .remove_from_connection,\n\t\t# so we need a copy.\n\t\tpath = self._get_path()\n\t\tif path is None:\n\t\t\treturn\n\t\tself.remove_from_connection()\n\t\tlogging.debug(\"VeDbusTreeExport %s has been removed\" % path)\n\n\tdef _get_path(self):\n\t\tif len(self._locations) == 0:\n\t\t\treturn None\n\t\treturn self._locations[0][1]\n\n\tdef _get_value_handler(self, path, get_text=False):\n\t\tlogging.debug(\"_get_value_handler called for %s\" % path)\n\t\tr = {}\n\t\tpx = path\n\t\tif not px.endswith('/'):\n\t\t\tpx += '/'\n\t\tfor p, item in self._service._dbusobjects.items():\n\t\t\tif p.startswith(px):\n\t\t\t\tv = item.GetText() if get_text else wrap_dbus_value(item.local_get_value())\n\t\t\t\tr[p[len(px):]] = v\n\t\tlogging.debug(r)\n\t\treturn r\n\n\t@dbus.service.method('com.victronenergy.BusItem', out_signature='v')\n\tdef GetValue(self):\n\t\tvalue = self._get_value_handler(self._get_path())\n\t\treturn dbus.Dictionary(value, signature=dbus.Signature('sv'), variant_level=1)\n\n\t@dbus.service.method('com.victronenergy.BusItem', out_signature='v')\n\tdef GetText(self):\n\t\treturn self._get_value_handler(self._get_path(), True)\n\n\tdef local_get_value(self):\n\t\treturn self._get_value_handler(self.path)\n\nclass VeDbusRootExport(VeDbusTreeExport):\n\t@dbus.service.signal('com.victronenergy.BusItem', signature='a{sa{sv}}')\n\tdef ItemsChanged(self, changes):\n\t\tpass\n\n\t@dbus.service.method('com.victronenergy.BusItem', out_signature='a{sa{sv}}')\n\tdef GetItems(self):\n\t\treturn {\n\t\t\tpath: {\n\t\t\t\t'Value': wrap_dbus_value(item.local_get_value()),\n\t\t\t\t'Text': item.GetText() }\n\t\t\tfor path, item in self._service._dbusobjects.items()\n\t\t}\n\n\nclass VeDbusItemExport(dbus.service.Object):\n\t## Constructor of VeDbusItemExport\n\t#\n\t# Use this object to export (publish), values on the dbus\n\t# Creates the dbus-object under the given dbus-service-name.\n\t# @param bus\t\t  The dbus object.\n\t# @param objectPath\t  The dbus-object-path.\n\t# @param value\t\t  Value to initialize ourselves with, defaults to None which means Invalid\n\t# @param description  String containing a description. Can be called over the dbus with GetDescription()\n\t# @param writeable\t  what would this do!? :).\n\t# @param callback\t  Function that will be called when someone else changes the value of this VeBusItem\n\t#                     over the dbus. First parameter passed to callback will be our path, second the new\n\t#\t\t\t\t\t  value. This callback should return True to accept the change, False to reject it.\n\tdef __init__(self, bus, objectPath, value=None, description=None, writeable=False,\n\t\t\t\t\tonchangecallback=None, gettextcallback=None, deletecallback=None,\n\t\t\t\t\tvaluetype=None):\n\t\tdbus.service.Object.__init__(self, bus, objectPath)\n\t\tself._onchangecallback = onchangecallback\n\t\tself._gettextcallback = gettextcallback\n\t\tself._value = value\n\t\tself._description = description\n\t\tself._writeable = writeable\n\t\tself._deletecallback = deletecallback\n\t\tself._type = valuetype\n\n\t# To force immediate deregistering of this dbus object, explicitly call __del__().\n\tdef __del__(self):\n\t\t# self._get_path() will raise an exception when retrieved after the\n\t\t# call to .remove_from_connection, so we need a copy.\n\t\tpath = self._get_path()\n\t\tif path == None:\n\t\t\treturn\n\t\tif self._deletecallback is not None:\n\t\t\tself._deletecallback(path)\n\t\tself.remove_from_connection()\n\t\tlogging.debug(\"VeDbusItemExport %s has been removed\" % path)\n\n\tdef _get_path(self):\n\t\tif len(self._locations) == 0:\n\t\t\treturn None\n\t\treturn self._locations[0][1]\n\n\t## Sets the value. And in case the value is different from what it was, a signal\n\t# will be emitted to the dbus. This function is to be used in the python code that\n\t# is using this class to export values to the dbus.\n\t# set value to None to indicate that it is Invalid\n\tdef local_set_value(self, newvalue):\n\t\tchanges = self._local_set_value(newvalue)\n\t\tif changes is not None:\n\t\t\tself.PropertiesChanged(changes)\n\n\tdef _local_set_value(self, newvalue):\n\t\tif self._value == newvalue:\n\t\t\treturn None\n\n\t\tself._value = newvalue\n\t\treturn {\n\t\t\t'Value': wrap_dbus_value(newvalue),\n\t\t\t'Text': self.GetText()\n\t\t}\n\n\tdef local_get_value(self):\n\t\treturn self._value\n\n\t# ==== ALL FUNCTIONS BELOW THIS LINE WILL BE CALLED BY OTHER PROCESSES OVER THE DBUS ====\n\n\t## Dbus exported method SetValue\n\t# Function is called over the D-Bus by other process. It will first check (via callback) if new\n\t# value is accepted. And it is, stores it and emits a changed-signal.\n\t# @param value The new value.\n\t# @return completion-code When successful a 0 is return, and when not a -1 is returned.\n\t@dbus.service.method('com.victronenergy.BusItem', in_signature='v', out_signature='i')\n\tdef SetValue(self, newvalue):\n\t\tif not self._writeable:\n\t\t\treturn 1  # NOT OK\n\n\t\tnewvalue = unwrap_dbus_value(newvalue)\n\n\t\t# If value type is enforced, cast it. If the type can be coerced\n\t\t# python will do it for us. This allows ints to become floats,\n\t\t# or bools to become ints. Additionally also allow None, so that\n\t\t# a path may be invalidated.\n\t\tif self._type is not None and newvalue is not None:\n\t\t\ttry:\n\t\t\t\tnewvalue = self._type(newvalue)\n\t\t\texcept (ValueError, TypeError):\n\t\t\t\treturn 1 # NOT OK\n\n\t\tif newvalue == self._value:\n\t\t\treturn 0  # OK\n\n\t\t# call the callback given to us, and check if new value is OK.\n\t\tif (self._onchangecallback is None or\n\t\t\t\t(self._onchangecallback is not None and self._onchangecallback(self.__dbus_object_path__, newvalue))):\n\n\t\t\tself.local_set_value(newvalue)\n\t\t\treturn 0  # OK\n\n\t\treturn 2  # NOT OK\n\n\t## Dbus exported method GetDescription\n\t#\n\t# Returns the a description.\n\t# @param language A language code (e.g. ISO 639-1 en-US).\n\t# @param length Lenght of the language string.\n\t# @return description\n\t@dbus.service.method('com.victronenergy.BusItem', in_signature='si', out_signature='s')\n\tdef GetDescription(self, language, length):\n\t\treturn self._description if self._description is not None else 'No description given'\n\n\t## Dbus exported method GetValue\n\t# Returns the value.\n\t# @return the value when valid, and otherwise an empty array\n\t@dbus.service.method('com.victronenergy.BusItem', out_signature='v')\n\tdef GetValue(self):\n\t\treturn wrap_dbus_value(self._value)\n\n\t## Dbus exported method GetText\n\t# Returns the value as string of the dbus-object-path.\n\t# @return text A text-value. '---' when local value is invalid\n\t@dbus.service.method('com.victronenergy.BusItem', out_signature='s')\n\tdef GetText(self):\n\t\tif self._value is None:\n\t\t\treturn '---'\n\n\t\t# Default conversion from dbus.Byte will get you a character (so 'T' instead of '84'), so we\n\t\t# have to convert to int first. Note that if a dbus.Byte turns up here, it must have come from\n\t\t# the application itself, as all data from the D-Bus should have been unwrapped by now.\n\t\tif self._gettextcallback is None and type(self._value) == dbus.Byte:\n\t\t\treturn str(int(self._value))\n\n\t\tif self._gettextcallback is None and self.__dbus_object_path__ == '/ProductId':\n\t\t\treturn \"0x%X\" % self._value\n\n\t\tif self._gettextcallback is None:\n\t\t\treturn str(self._value)\n\n\t\treturn self._gettextcallback(self.__dbus_object_path__, self._value)\n\n\t## The signal that indicates that the value has changed.\n\t# Other processes connected to this BusItem object will have subscribed to the\n\t# event when they want to track our state.\n\t@dbus.service.signal('com.victronenergy.BusItem', signature='a{sv}')\n\tdef PropertiesChanged(self, changes):\n\t\tpass\n\n## This class behaves like a regular reference to a class method (eg. self.foo), but keeps a weak reference\n## to the object which method is to be called.\n## Use this object to break circular references.\nclass weak_functor:\n\tdef __init__(self, f):\n\t\tself._r = weakref.ref(f.__self__)\n\t\tself._f = weakref.ref(f.__func__)\n\n\tdef __call__(self, *args, **kargs):\n\t\tr = self._r()\n\t\tf = self._f()\n\t\tif r == None or f == None:\n\t\t\treturn\n\t\tf(r, *args, **kargs)\n"
  },
  {
    "path": "velib_python/velib_python/v3.41/dbusmonitor.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\n## @package dbus_vrm\n# This code takes care of the D-Bus interface (not all of below is implemented yet):\n# - on startup it scans the dbus for services we know. For each known service found, it searches for\n#   objects/paths we know. Everything we find is stored in items{}, and an event is registered: if a\n#   value changes weĺl be notified and can pass that on to our owner. For example the vrmLogger.\n#   we know.\n# - after startup, it continues to monitor the dbus:\n#\t\t1) when services are added we do the same check on that\n#\t\t2) when services are removed, we remove any items that we had that referred to that service\n#\t\t3) if an existing services adds paths we update ourselves as well: on init, we make a\n#\t\t   VeDbusItemImport for a non-, or not yet existing objectpaths as well1\n#\n# Code is used by the vrmLogger, and also the pubsub code. Both are other modules in the dbus_vrm repo.\n\nfrom dbus.mainloop.glib import DBusGMainLoop\nfrom gi.repository import GLib\nimport dbus\nimport dbus.service\nimport inspect\nimport logging\nimport argparse\nimport pprint\nimport traceback\nimport os\nfrom collections import defaultdict\nfrom functools import partial\n\n# our own packages\nfrom ve_utils import exit_on_error, wrap_dbus_value, unwrap_dbus_value, add_name_owner_changed_receiver\nnotfound = object() # For lookups where None is a valid result\n\nlogger = logging.getLogger(__name__)\nlogger.setLevel(logging.INFO)\nclass SystemBus(dbus.bus.BusConnection):\n\tdef __new__(cls):\n\t\treturn dbus.bus.BusConnection.__new__(cls, dbus.bus.BusConnection.TYPE_SYSTEM)\n\nclass SessionBus(dbus.bus.BusConnection):\n\tdef __new__(cls):\n\t\treturn dbus.bus.BusConnection.__new__(cls, dbus.bus.BusConnection.TYPE_SESSION)\n\nclass MonitoredValue(object):\n\tdef __init__(self, value, text, options):\n\t\tsuper(MonitoredValue, self).__init__()\n\t\tself.value = value\n\t\tself.text = text\n\t\tself.options = options\n\n\t# For legacy code, allow treating this as a tuple/list\n\tdef __iter__(self):\n\t\treturn iter((self.value, self.text, self.options))\n\nclass Service(object):\n\tdef __init__(self, id, serviceName, deviceInstance):\n\t\tsuper(Service, self).__init__()\n\t\tself.id = id\n\t\tself.name = serviceName\n\t\tself.paths = {}\n\t\tself._seen = set()\n\t\tself.deviceInstance = deviceInstance\n\n\t# For legacy code, attributes can still be accessed as if keys from a\n\t# dictionary.\n\tdef __setitem__(self, key, value):\n\t\tself.__dict__[key] = value\n\tdef __getitem__(self, key):\n\t\treturn self.__dict__[key]\n\n\tdef set_seen(self, path):\n\t\tself._seen.add(path)\n\n\tdef seen(self, path):\n\t\treturn path in self._seen\n\n\t@property\n\tdef service_class(self):\n\t\treturn '.'.join(self.name.split('.')[:3])\n\nclass DbusMonitor(object):\n\t## Constructor\n\tdef __init__(self, dbusTree, valueChangedCallback=None, deviceAddedCallback=None,\n\t\t\t\t\tdeviceRemovedCallback=None, namespace=\"com.victronenergy\", ignoreServices=[]):\n\t\t# valueChangedCallback is the callback that we call when something has changed.\n\t\t# def value_changed_on_dbus(dbusServiceName, dbusPath, options, changes, deviceInstance):\n\t\t# in which changes is a tuple with GetText() and GetValue()\n\t\tself.valueChangedCallback = valueChangedCallback\n\t\tself.deviceAddedCallback = deviceAddedCallback\n\t\tself.deviceRemovedCallback = deviceRemovedCallback\n\t\tself.dbusTree = dbusTree\n\t\tself.ignoreServices = ignoreServices\n\n\t\t# Lists all tracked services. Stores name, id, device instance, value per path, and whenToLog info\n\t\t# indexed by service name (eg. com.victronenergy.settings).\n\t\tself.servicesByName = {}\n\n\t\t# Same values as self.servicesByName, but indexed by service id (eg. :1.30)\n\t\tself.servicesById = {}\n\n\t\t# Keep track of services by class to speed up calls to get_service_list\n\t\tself.servicesByClass = defaultdict(list)\n\n\t\t# Keep track of any additional watches placed on items\n\t\tself.serviceWatches = defaultdict(list)\n\n\t\t# For a PC, connect to the SessionBus\n\t\t# For a CCGX, connect to the SystemBus\n\t\tself.dbusConn = SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else SystemBus()\n\n\t\t# subscribe to NameOwnerChange for bus connect / disconnect events.\n\t\t# NOTE: this is on a different bus then the one above!\n\t\tstandardBus = (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ \\\n\t\t\telse dbus.SystemBus())\n\n\t\tadd_name_owner_changed_receiver(standardBus, self.dbus_name_owner_changed)\n\n\t\t# Subscribe to PropertiesChanged for all services\n\t\tself.dbusConn.add_signal_receiver(self.handler_value_changes,\n\t\t\tdbus_interface='com.victronenergy.BusItem',\n\t\t\tsignal_name='PropertiesChanged', path_keyword='path',\n\t\t\tsender_keyword='senderId')\n\n\t\t# Subscribe to ItemsChanged for all services\n\t\tself.dbusConn.add_signal_receiver(self.handler_item_changes,\n\t\t\tdbus_interface='com.victronenergy.BusItem',\n\t\t\tsignal_name='ItemsChanged', path='/',\n\t\t\tsender_keyword='senderId')\n\n\t\tlogger.info('===== Search on dbus for services that we will monitor starting... =====')\n\t\tserviceNames = self.dbusConn.list_names()\n\t\tfor serviceName in serviceNames:\n\t\t\tself.scan_dbus_service(serviceName)\n\n\t\tlogger.info('===== Search on dbus for services that we will monitor finished =====')\n\n\t@staticmethod\n\tdef make_service(serviceId, serviceName, deviceInstance):\n\t\t\"\"\" Override this to use a different kind of service object. \"\"\"\n\t\treturn Service(serviceId, serviceName, deviceInstance)\n\n\tdef make_monitor(self, service, path, value, text, options):\n\t\t\"\"\" Override this to do more things with monitoring. \"\"\"\n\t\treturn MonitoredValue(unwrap_dbus_value(value), unwrap_dbus_value(text), options)\n\n\tdef dbus_name_owner_changed(self, name, oldowner, newowner):\n\t\tif not name.startswith(\"com.victronenergy.\"):\n\t\t\treturn\n\n\t\t#decouple, and process in main loop\n\t\tGLib.idle_add(exit_on_error, self._process_name_owner_changed, name, oldowner, newowner)\n\n\tdef _process_name_owner_changed(self, name, oldowner, newowner):\n\t\tif newowner != '':\n\t\t\t# so we found some new service. Check if we can do something with it.\n\t\t\tnewdeviceadded = self.scan_dbus_service(name)\n\t\t\tif newdeviceadded and self.deviceAddedCallback is not None:\n\t\t\t\tself.deviceAddedCallback(name, self.get_device_instance(name))\n\n\t\telif name in self.servicesByName:\n\t\t\t# it disappeared, we need to remove it.\n\t\t\tlogger.info(\"%s disappeared from the dbus. Removing it from our lists\" % name)\n\t\t\tservice = self.servicesByName[name]\n\t\t\tdel self.servicesById[service.id]\n\t\t\tdel self.servicesByName[name]\n\t\t\tfor watch in self.serviceWatches[name]:\n\t\t\t\twatch.remove()\n\t\t\tdel self.serviceWatches[name]\n\t\t\tself.servicesByClass[service.service_class].remove(service)\n\t\t\tif self.deviceRemovedCallback is not None:\n\t\t\t\tself.deviceRemovedCallback(name, service.deviceInstance)\n\n\tdef scan_dbus_service(self, serviceName):\n\t\ttry:\n\t\t\treturn self.scan_dbus_service_inner(serviceName)\n\t\texcept:\n\t\t\tlogger.error(\"Ignoring %s because of error while scanning:\" % (serviceName))\n\t\t\ttraceback.print_exc()\n\t\t\treturn False\n\n\t\t\t# Errors 'org.freedesktop.DBus.Error.ServiceUnknown' and\n\t\t\t# 'org.freedesktop.DBus.Error.Disconnected' seem to happen when the service\n\t\t\t# disappears while its being scanned. Which might happen, but is not really\n\t\t\t# normal either, so letting them go into the logs.\n\n\t# Scans the given dbus service to see if it contains anything interesting for us. If it does, add\n\t# it to our list of monitored D-Bus services.\n\tdef scan_dbus_service_inner(self, serviceName):\n\n\t\t# make it a normal string instead of dbus string\n\t\tserviceName = str(serviceName)\n\n\t\tif (len(self.ignoreServices) != 0 and any(serviceName.startswith(x) for x in self.ignoreServices)):\n\t\t\tlogger.debug(\"Ignoring service %s\" % serviceName)\n\t\t\treturn False\n\n\t\tpaths = self.dbusTree.get('.'.join(serviceName.split('.')[0:3]), None)\n\t\tif paths is None:\n\t\t\tlogger.debug(\"Ignoring service %s, not in the tree\" % serviceName)\n\t\t\treturn False\n\n\t\tlogger.info(\"Found: %s, scanning and storing items\" % serviceName)\n\t\tserviceId = self.dbusConn.get_name_owner(serviceName)\n\n\t\t# we should never be notified to add a D-Bus service that we already have. If this assertion\n\t\t# raises, check process_name_owner_changed, and D-Bus workings.\n\t\tassert serviceName not in self.servicesByName\n\t\tassert serviceId not in self.servicesById\n\n\t\t# Try to fetch everything with a GetItems, then fall back to older\n\t\t# methods if that fails\n\t\ttry:\n\t\t\tvalues = self.dbusConn.call_blocking(serviceName, '/', None, 'GetItems', '', [])\n\t\texcept dbus.exceptions.DBusException:\n\t\t\tlogger.info(\"GetItems failed, trying legacy methods\")\n\t\telse:\n\t\t\treturn self.scan_dbus_service_getitems_done(serviceName, serviceId, values)\n\n\t\tif serviceName == 'com.victronenergy.settings':\n\t\t\tdi = 0\n\t\telif serviceName.startswith('com.victronenergy.vecan.'):\n\t\t\tdi = 0\n\t\telse:\n\t\t\ttry:\n\t\t\t\tdi = self.dbusConn.call_blocking(serviceName,\n\t\t\t\t\t'/DeviceInstance', None, 'GetValue', '', [])\n\t\t\texcept dbus.exceptions.DBusException:\n\t\t\t\tlogger.info(\"       %s was skipped because it has no device instance\" % serviceName)\n\t\t\t\treturn False # Skip it\n\t\t\telse:\n\t\t\t\tdi = int(di)\n\n\t\tlogger.info(\"       %s has device instance %s\" % (serviceName, di))\n\t\tservice = self.make_service(serviceId, serviceName, di)\n\n\t\t# Let's try to fetch everything in one go\n\t\tvalues = {}\n\t\ttexts = {}\n\t\ttry:\n\t\t\tvalues.update(self.dbusConn.call_blocking(serviceName, '/', None, 'GetValue', '', []))\n\t\t\ttexts.update(self.dbusConn.call_blocking(serviceName, '/', None, 'GetText', '', []))\n\t\texcept:\n\t\t\tpass\n\n\t\tfor path, options in paths.items():\n\t\t\t# path will be the D-Bus path: '/Ac/ActiveIn/L1/V'\n\t\t\t# options will be a dictionary: {'code': 'V', 'whenToLog': 'onIntervalAlways'}\n\n\t\t\t# Try to obtain the value we want from our bulk fetch. If we\n\t\t\t# cannot find it there, do an individual query.\n\t\t\tvalue = values.get(path[1:], notfound)\n\t\t\tif value != notfound:\n\t\t\t\tservice.set_seen(path)\n\t\t\ttext = texts.get(path[1:], notfound)\n\t\t\tif value is notfound or text is notfound:\n\t\t\t\ttry:\n\t\t\t\t\tvalue = self.dbusConn.call_blocking(serviceName, path, None, 'GetValue', '', [])\n\t\t\t\t\tservice.set_seen(path)\n\t\t\t\t\ttext = self.dbusConn.call_blocking(serviceName, path, None, 'GetText', '', [])\n\t\t\t\texcept dbus.exceptions.DBusException as e:\n\t\t\t\t\tif e.get_dbus_name() in (\n\t\t\t\t\t\t\t'org.freedesktop.DBus.Error.ServiceUnknown',\n\t\t\t\t\t\t\t'org.freedesktop.DBus.Error.Disconnected'):\n\t\t\t\t\t\traise # This exception will be handled below\n\n\t\t\t\t\t# TODO org.freedesktop.DBus.Error.UnknownMethod really\n\t\t\t\t\t# shouldn't happen but sometimes does.\n\t\t\t\t\tlogger.debug(\"%s %s does not exist (yet)\" % (serviceName, path))\n\t\t\t\t\tvalue = None\n\t\t\t\t\ttext = None\n\n\t\t\tservice.paths[path] = self.make_monitor(service, path, unwrap_dbus_value(value), unwrap_dbus_value(text), options)\n\n\n\t\tlogger.debug(\"Finished scanning and storing items for %s\" % serviceName)\n\n\t\t# Adjust self at the end of the scan, so we don't have an incomplete set of\n\t\t# data if an exception occurs during the scan.\n\t\tself.servicesByName[serviceName] = service\n\t\tself.servicesById[serviceId] = service\n\t\tself.servicesByClass[service.service_class].append(service)\n\n\t\treturn True\n\n\tdef scan_dbus_service_getitems_done(self, serviceName, serviceId, values):\n\t\t# Keeping these exceptions for legacy reasons\n\t\tif serviceName == 'com.victronenergy.settings':\n\t\t\tdi = 0\n\t\telif serviceName.startswith('com.victronenergy.vecan.'):\n\t\t\tdi = 0\n\t\telse:\n\t\t\ttry:\n\t\t\t\tdi = values['/DeviceInstance']['Value']\n\t\t\texcept KeyError:\n\t\t\t\tlogger.info(\"       %s was skipped because it has no device instance\" % serviceName)\n\t\t\t\treturn False\n\t\t\telse:\n\t\t\t\tdi = int(di)\n\n\t\tlogger.info(\"       %s has device instance %s\" % (serviceName, di))\n\t\tservice = self.make_service(serviceId, serviceName, di)\n\n\t\tpaths = self.dbusTree.get('.'.join(serviceName.split('.')[0:3]), {})\n\t\tfor path, options in paths.items():\n\t\t\titem = values.get(path, notfound)\n\t\t\tif item is notfound:\n\t\t\t\tservice.paths[path] = self.make_monitor(service, path, None, None, options)\n\t\t\telse:\n\t\t\t\tservice.set_seen(path)\n\t\t\t\tvalue = item.get('Value', None)\n\t\t\t\ttext = item.get('Text', None)\n\t\t\t\tservice.paths[path] = self.make_monitor(service, path, unwrap_dbus_value(value), unwrap_dbus_value(text), options)\n\n\t\tself.servicesByName[serviceName] = service\n\t\tself.servicesById[serviceId] = service\n\t\tself.servicesByClass[service.service_class].append(service)\n\t\treturn True\n\n\tdef handler_item_changes(self, items, senderId):\n\t\tif not isinstance(items, dict):\n\t\t\treturn\n\n\t\ttry:\n\t\t\tservice = self.servicesById[senderId]\n\t\texcept KeyError:\n\t\t\t# senderId isn't there, which means it hasn't been scanned yet.\n\t\t\treturn\n\n\t\tfor path, changes in items.items():\n\t\t\ttry:\n\t\t\t\tv = unwrap_dbus_value(changes['Value'])\n\t\t\texcept (KeyError, TypeError):\n\t\t\t\tcontinue\n\n\t\t\ttry:\n\t\t\t\tt = changes['Text']\n\t\t\texcept KeyError:\n\t\t\t\tt = str(v)\n\t\t\tself._handler_value_changes(service, path, v, t)\n\n\tdef handler_value_changes(self, changes, path, senderId):\n\t\t# If this properyChange does not involve a value, our work is done.\n\t\tif 'Value' not in changes:\n\t\t\treturn\n\n\t\ttry:\n\t\t\tservice = self.servicesById[senderId]\n\t\texcept KeyError:\n\t\t\t# senderId isn't there, which means it hasn't been scanned yet.\n\t\t\treturn\n\n\t\tv = unwrap_dbus_value(changes['Value'])\n\t\t# Some services don't send Text with their PropertiesChanged events.\n\t\ttry:\n\t\t\tt = changes['Text']\n\t\texcept KeyError:\n\t\t\tt = str(v)\n\t\tself._handler_value_changes(service, path, v, t)\n\n\tdef _handler_value_changes(self, service, path, value, text):\n\t\ttry:\n\t\t\ta = service.paths[path]\n\t\texcept KeyError:\n\t\t\t# path isn't there, which means it hasn't been scanned yet.\n\t\t\treturn\n\n\t\tservice.set_seen(path)\n\n\t\t# First update our store to the new value\n\t\tif a.value == value:\n\t\t\treturn\n\n\t\ta.value = value\n\t\ta.text = text\n\n\t\t# And do the rest of the processing in on the mainloop\n\t\tif self.valueChangedCallback is not None:\n\t\t\tGLib.idle_add(exit_on_error, self._execute_value_changes, service.name, path, {\n\t\t\t\t'Value': value, 'Text': text}, a.options)\n\n\tdef _execute_value_changes(self, serviceName, objectPath, changes, options):\n\t\t# double check that the service still exists, as it might have\n\t\t# disappeared between scheduling-for and executing this function.\n\t\tif serviceName not in self.servicesByName:\n\t\t\treturn\n\n\t\tself.valueChangedCallback(serviceName, objectPath,\n\t\t\toptions, changes, self.get_device_instance(serviceName))\n\n\t# Gets the value for a certain servicename and path\n\t# The default_value is returned when:\n\t# 1. When the service doesn't exist.\n\t# 2. When the path asked for isn't being monitored.\n\t# 3. When the path exists, but has dbus-invalid, ie an empty byte array.\n\t# 4. When the path asked for is being monitored, but doesn't exist for that service.\n\tdef get_value(self, serviceName, objectPath, default_value=None):\n\t\tservice = self.servicesByName.get(serviceName, None)\n\t\tif service is None:\n\t\t\treturn default_value\n\n\t\tvalue = service.paths.get(objectPath, None)\n\t\tif value is None or value.value is None:\n\t\t\treturn default_value\n\n\t\treturn value.value\n\n\t# returns if a dbus exists now, by doing a blocking dbus call.\n\t# Typically seen will be sufficient and doesn't need access to the dbus.\n\tdef exists(self, serviceName, objectPath):\n\t\ttry:\n\t\t\tself.dbusConn.call_blocking(serviceName, objectPath, None, 'GetValue', '', [])\n\t\t\treturn True\n\t\texcept dbus.exceptions.DBusException as e:\n\t\t\treturn False\n\n\t# Returns if there ever was a successful GetValue or valueChanged event.\n\t# Unlike get_value this return True also if the actual value is invalid.\n\t#\n\t# Note: the path might no longer exists anymore, but that doesn't happen in\n\t# practice. If a service really wants to reconfigure itself typically it should\n\t# reconnect to the dbus which causes it to be rescanned and seen will be updated.\n\t# If it is really needed to know if a path still exists, use exists.\n\tdef seen(self, serviceName, objectPath):\n\t\ttry:\n\t\t\treturn self.servicesByName[serviceName].seen(objectPath)\n\t\texcept KeyError:\n\t\t\treturn False\n\n\t# Sets the value for a certain servicename and path, returns the return value of the D-Bus SetValue\n\t# method. If the underlying item does not exist (the service does not exist, or the objectPath was not\n\t# registered) the function will return -1\n\tdef set_value(self, serviceName, objectPath, value):\n\t\t# Check if the D-Bus object referenced by serviceName and objectPath is registered. There is no\n\t\t# necessity to do this, but it is in line with previous implementations which kept VeDbusItemImport\n\t\t# objects for registers items only.\n\t\tservice = self.servicesByName.get(serviceName, None)\n\t\tif service is None:\n\t\t\treturn -1\n\t\tif objectPath not in service.paths:\n\t\t\treturn -1\n\t\t# We do not catch D-Bus exceptions here, because the previous implementation did not do that either.\n\t\treturn self.dbusConn.call_blocking(serviceName, objectPath,\n\t\t\t\t   dbus_interface='com.victronenergy.BusItem',\n\t\t\t\t   method='SetValue', signature=None,\n\t\t\t\t   args=[wrap_dbus_value(value)])\n\n\t# Similar to set_value, but operates asynchronously\n\tdef set_value_async(self, serviceName, objectPath, value,\n\t\t\treply_handler=None, error_handler=None):\n\t\tservice = self.servicesByName.get(serviceName, None)\n\t\tif service is not None:\n\t\t\tif objectPath in service.paths:\n\t\t\t\tself.dbusConn.call_async(serviceName, objectPath,\n\t\t\t\t\tdbus_interface='com.victronenergy.BusItem',\n\t\t\t\t\tmethod='SetValue', signature=None,\n\t\t\t\t\targs=[wrap_dbus_value(value)],\n\t\t\t\t\treply_handler=reply_handler, error_handler=error_handler)\n\t\t\t\treturn\n\n\t\tif error_handler is not None:\n\t\t\terror_handler(TypeError('Service or path not found, '\n\t\t\t\t\t\t'service=%s, path=%s' % (serviceName, objectPath)))\n\n\t# returns a dictionary, keys are the servicenames, value the instances\n\t# optionally use the classfilter to get only a certain type of services, for\n\t# example com.victronenergy.battery.\n\tdef get_service_list(self, classfilter=None):\n\t\tif classfilter is None:\n\t\t\treturn { servicename: service.deviceInstance \\\n\t\t\t\tfor servicename, service in self.servicesByName.items() }\n\n\t\tif classfilter not in self.servicesByClass:\n\t\t\treturn {}\n\n\t\treturn { service.name: service.deviceInstance \\\n\t\t\tfor service in self.servicesByClass[classfilter] }\n\n\tdef get_device_instance(self, serviceName):\n\t\treturn self.servicesByName[serviceName].deviceInstance\n\n\tdef track_value(self, serviceName, objectPath, callback, *args, **kwargs):\n\t\t\"\"\" A DbusMonitor can watch specific service/path combos for changes\n\t\t    so that it is not fully reliant on the global handler_value_changes\n\t\t    in this class. Additional watches are deleted automatically when\n\t\t    the service disappears from dbus. \"\"\"\n\t\tcb = partial(callback, *args, **kwargs)\n\n\t\tdef root_tracker(items):\n\t\t\t# Check if objectPath in dict\n\t\t\ttry:\n\t\t\t\tv = items[objectPath]\n\t\t\t\t_v = unwrap_dbus_value(v['Value'])\n\t\t\texcept (KeyError, TypeError):\n\t\t\t\treturn # not in this dict\n\n\t\t\ttry:\n\t\t\t\tt = v['Text']\n\t\t\texcept KeyError:\n\t\t\t\tcb({'Value': _v })\n\t\t\telse:\n\t\t\t\tcb({'Value': _v, 'Text': t})\n\n\t\t# Track changes on the path, and also on root\n\t\tself.serviceWatches[serviceName].extend((\n\t\t\tself.dbusConn.add_signal_receiver(cb,\n\t\t\t\tdbus_interface='com.victronenergy.BusItem',\n\t\t\t\tsignal_name='PropertiesChanged',\n\t\t\t\tpath=objectPath, bus_name=serviceName),\n\t\t\tself.dbusConn.add_signal_receiver(root_tracker,\n\t\t\t\tdbus_interface='com.victronenergy.BusItem',\n\t\t\t\tsignal_name='ItemsChanged',\n\t\t\t\tpath=\"/\", bus_name=serviceName),\n\t\t))\n\n\n# ====== ALL CODE BELOW THIS LINE IS PURELY FOR DEVELOPING THIS CLASS ======\n\n# Example function that can be used as a starting point to use this code\ndef value_changed_on_dbus(dbusServiceName, dbusPath, dict, changes, deviceInstance):\n\tlogger.debug(\"0 ----------------\")\n\tlogger.debug(\"1 %s%s changed\" % (dbusServiceName, dbusPath))\n\tlogger.debug(\"2 vrm dict     : %s\" % dict)\n\tlogger.debug(\"3 changes-text: %s\" % changes['Text'])\n\tlogger.debug(\"4 changes-value: %s\" % changes['Value'])\n\tlogger.debug(\"5 deviceInstance: %s\" % deviceInstance)\n\tlogger.debug(\"6 - end\")\n\n\ndef nameownerchange(a, b):\n\t# used to find memory leaks in dbusmonitor and VeDbusItemImport\n\timport gc\n\tgc.collect()\n\tobjects = gc.get_objects()\n\tprint (len([o for o in objects if type(o).__name__ == 'VeDbusItemImport']))\n\tprint (len([o for o in objects if type(o).__name__ == 'SignalMatch']))\n\tprint (len(objects))\n\n\ndef print_values(dbusmonitor):\n\ta = dbusmonitor.get_value('wrongservice', '/DbusInvalid', default_value=1000)\n\tb = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/NotInTheMonitorList', default_value=1000)\n\tc = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/DbusInvalid', default_value=1000)\n\td = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/NonExistingButMonitored', default_value=1000)\n\n\tprint (\"All should be 1000: Wrong Service: %s, NotInTheMonitorList: %s, DbusInvalid: %s, NonExistingButMonitored: %s\" % (a, b, c, d))\n\treturn True\n\n# We have a mainloop, but that is just for developing this code. Normally above class & code is used from\n# some other class, such as vrmLogger or the pubsub Implementation.\ndef main():\n\t# Init logging\n\tlogging.basicConfig(level=logging.DEBUG)\n\tlogger.info(__file__ + \" is starting up\")\n\n\t# Have a mainloop, so we can send/receive asynchronous calls to and from dbus\n\tDBusGMainLoop(set_as_default=True)\n\n\timport os\n\timport sys\n\tsys.path.insert(1, os.path.join(os.path.dirname(__file__), '../../'))\n\n\tdummy = {'code': None, 'whenToLog': 'configChange', 'accessLevel': None}\n\tmonitorlist = {'com.victronenergy.dummyservice': {\n\t\t\t\t'/Connected': dummy,\n\t\t\t\t'/ProductName': dummy,\n\t\t\t\t'/Mgmt/Connection': dummy,\n\t\t\t\t'/Dc/0/Voltage': dummy,\n\t\t\t\t'/Dc/0/Current': dummy,\n\t\t\t\t'/Dc/0/Temperature': dummy,\n\t\t\t\t'/Load/I': dummy,\n\t\t\t\t'/FirmwareVersion': dummy,\n\t\t\t\t'/DbusInvalid': dummy,\n\t\t\t\t'/NonExistingButMonitored': dummy}}\n\n\td = DbusMonitor(monitorlist, value_changed_on_dbus,\n\t\tdeviceAddedCallback=nameownerchange, deviceRemovedCallback=nameownerchange)\n\n\tGLib.timeout_add(1000, print_values, d)\n\n\t# Start and run the mainloop\n\tlogger.info(\"Starting mainloop, responding on only events\")\n\tmainloop = GLib.MainLoop()\n\tmainloop.run()\n\nif __name__ == \"__main__\":\n\tmain()\n"
  },
  {
    "path": "velib_python/velib_python/v3.41/oldestVersion",
    "content": "v3.40\n"
  },
  {
    "path": "velib_python/velib_python/v3.41/settingsdevice.py",
    "content": "import dbus\nimport logging\nimport time\nfrom functools import partial\n\n# Local imports\nfrom vedbus import VeDbusItemImport\n\n## Indexes for the setting dictonary.\nPATH = 0\nVALUE = 1\nMINIMUM = 2\nMAXIMUM = 3\nSILENT = 4\n\n## The Settings Device class.\n# Used by python programs, such as the vrm-logger, to read and write settings they\n# need to store on disk. And since these settings might be changed from a different\n# source, such as the GUI, the program can pass an eventCallback that will be called\n# as soon as some setting is changed.\n#\n# The settings are stored in flash via the com.victronenergy.settings service on dbus.\n# See https://github.com/victronenergy/localsettings for more info.\n#\n# If there are settings in de supportSettings list which are not yet on the dbus, \n# and therefore not yet in the xml file, they will be added through the dbus-addSetting\n# interface of com.victronenergy.settings.\nclass SettingsDevice(object):\n\t## The constructor processes the tree of dbus-items.\n\t# @param bus the system-dbus object\n\t# @param name the dbus-service-name of the settings dbus service, 'com.victronenergy.settings'\n\t# @param supportedSettings dictionary with all setting-names, and their defaultvalue, min, max and whether\n\t# the setting is silent. The 'silent' entry is optional. If set to true, no changes in the setting will\n\t# be logged by localsettings.\n\t# @param eventCallback function that will be called on changes on any of these settings\n\t# @param timeout Maximum interval to wait for localsettings. An exception is thrown at the end of the\n\t# interval if the localsettings D-Bus service has not appeared yet.\n\tdef __init__(self, bus, supportedSettings, eventCallback, name='com.victronenergy.settings', timeout=0):\n\t\tlogging.debug(\"===== Settings device init starting... =====\")\n\t\tself._bus = bus\n\t\tself._dbus_name = name\n\t\tself._eventCallback = eventCallback\n\t\tself._values = {} # stored the values, used to pass the old value along on a setting change\n\t\tself._settings = {}\n\n\t\tcount = 0\n\t\twhile True:\n\t\t\tif 'com.victronenergy.settings' in self._bus.list_names():\n\t\t\t\tbreak\n\t\t\tif count == timeout:\n\t\t\t\traise Exception(\"The settings service com.victronenergy.settings does not exist!\")\n\t\t\tcount += 1\n\t\t\tlogging.info('waiting for settings')\n\t\t\ttime.sleep(1)\n\n\t\t# Add the items.\n\t\tself.addSettings(supportedSettings)\n\n\t\tlogging.debug(\"===== Settings device init finished =====\")\n\n\tdef addSettings(self, settings):\n\t\tfor setting, options in settings.items():\n\t\t\tsilent = len(options) > SILENT and options[SILENT]\n\t\t\tbusitem = self.addSetting(options[PATH], options[VALUE],\n\t\t\t\toptions[MINIMUM], options[MAXIMUM], silent, callback=partial(self.handleChangedSetting, setting))\n\t\t\tself._settings[setting] = busitem\n\t\t\tself._values[setting] = busitem.get_value()\n\n\tdef addSetting(self, path, value, _min, _max, silent=False, callback=None):\n\t\tbusitem = VeDbusItemImport(self._bus, self._dbus_name, path, callback)\n\t\tif busitem.exists and (value, _min, _max, silent) == busitem._proxy.GetAttributes():\n\t\t\tlogging.debug(\"Setting %s found\" % path)\n\t\telse:\n\t\t\tlogging.info(\"Setting %s does not exist yet or must be adjusted\" % path)\n\n\t\t\t# Prepare to add the setting. Most dbus types extend the python\n\t\t\t# type so it is only necessary to additionally test for Int64.\n\t\t\tif isinstance(value, (int, dbus.Int64)):\n\t\t\t\titemType = 'i'\n\t\t\telif isinstance(value, float):\n\t\t\t\titemType = 'f'\n\t\t\telse:\n\t\t\t\titemType = 's'\n\n\t\t\t# Add the setting\n\t\t\t# TODO, make an object that inherits VeDbusItemImport, and complete the D-Bus settingsitem interface\n\t\t\tsettings_item = VeDbusItemImport(self._bus, self._dbus_name, '/Settings', createsignal=False)\n\t\t\tsetting_path = path.replace('/Settings/', '', 1)\n\t\t\tif silent:\n\t\t\t\tsettings_item._proxy.AddSilentSetting('', setting_path, value, itemType, _min, _max)\n\t\t\telse:\n\t\t\t\tsettings_item._proxy.AddSetting('', setting_path, value, itemType, _min, _max)\n\n\t\t\tbusitem = VeDbusItemImport(self._bus, self._dbus_name, path, callback)\n\n\t\treturn busitem\n\n\tdef handleChangedSetting(self, setting, servicename, path, changes):\n\t\toldvalue = self._values[setting] if setting in self._values else None\n\t\tself._values[setting] = changes['Value']\n\n\t\tif self._eventCallback is None:\n\t\t\treturn\n\n\t\tself._eventCallback(setting, oldvalue, changes['Value'])\n\n\tdef setDefault(self, path):\n                item = VeDbusItemImport(self._bus, self._dbus_name, path, createsignal=False)\n                item.set_default()\n\n\tdef __getitem__(self, setting):\n\t\treturn self._settings[setting].get_value()\n\n\tdef __setitem__(self, setting, newvalue):\n\t\tresult = self._settings[setting].set_value(newvalue)\n\t\tif result != 0:\n\t\t\t# Trying to make some false change to our own settings? How dumb!\n\t\t\tassert False\n"
  },
  {
    "path": "velib_python/velib_python/v3.41/ve_utils.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\nimport sys\nfrom traceback import print_exc\nfrom os import _exit as os_exit\nfrom os import statvfs\nfrom subprocess import check_output, CalledProcessError\nimport logging\nimport dbus\nlogger = logging.getLogger(__name__)\n\nVEDBUS_INVALID = dbus.Array([], signature=dbus.Signature('i'), variant_level=1)\n\nclass NoVrmPortalIdError(Exception):\n\tpass\n\n# Use this function to make sure the code quits on an unexpected exception. Make sure to use it\n# when using GLib.idle_add and also GLib.timeout_add.\n# Without this, the code will just keep running, since GLib does not stop the mainloop on an\n# exception.\n# Example: GLib.idle_add(exit_on_error, myfunc, arg1, arg2)\ndef exit_on_error(func, *args, **kwargs):\n\ttry:\n\t\treturn func(*args, **kwargs)\n\texcept:\n\t\ttry:\n\t\t\tprint ('exit_on_error: there was an exception. Printing stacktrace will be tried and then exit')\n\t\t\tprint_exc()\n\t\texcept:\n\t\t\tpass\n\n\t\t# sys.exit() is not used, since that throws an exception, which does not lead to a program\n\t\t# halt when used in a dbus callback, see connection.py in the Python/Dbus libraries, line 230.\n\t\tos_exit(1)\n\n\n__vrm_portal_id = None\ndef get_vrm_portal_id():\n\t# The original definition of the VRM Portal ID is that it is the mac\n\t# address of the onboard- ethernet port (eth0), stripped from its colons\n\t# (:) and lower case. This may however differ between platforms. On Venus\n\t# the task is therefore deferred to /sbin/get-unique-id so that a\n\t# platform specific method can be easily defined.\n\t#\n\t# If /sbin/get-unique-id does not exist, then use the ethernet address\n\t# of eth0. This also handles the case where velib_python is used as a\n\t# package install on a Raspberry Pi.\n\t#\n\t# On a Linux host where the network interface may not be eth0, you can set\n\t# the VRM_IFACE environment variable to the correct name.\n\n\tglobal __vrm_portal_id\n\n\tif __vrm_portal_id:\n\t\treturn __vrm_portal_id\n\n\tportal_id = None\n\n\t# First try the method that works if we don't have a data partition. This\n\t# will fail when the current user is not root.\n\ttry:\n\t\tportal_id = check_output(\"/sbin/get-unique-id\").decode(\"utf-8\", \"ignore\").strip()\n\t\tif not portal_id:\n\t\t\traise NoVrmPortalIdError(\"get-unique-id returned blank\")\n\t\t__vrm_portal_id = portal_id\n\t\treturn portal_id\n\texcept CalledProcessError:\n\t\t# get-unique-id returned non-zero\n\t\traise NoVrmPortalIdError(\"get-unique-id returned non-zero\")\n\texcept OSError:\n\t\t# File doesn't exist, use fallback\n\t\tpass\n\n\t# Fall back to getting our id using a syscall. Assume we are on linux.\n\t# Allow the user to override what interface is used using an environment\n\t# variable.\n\timport fcntl, socket, struct, os\n\n\tiface = os.environ.get('VRM_IFACE', 'eth0').encode('ascii')\n\ts = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)\n\ttry:\n\t\tinfo = fcntl.ioctl(s.fileno(), 0x8927,  struct.pack('256s', iface[:15]))\n\texcept IOError:\n\t\traise NoVrmPortalIdError(\"ioctl failed for eth0\")\n\n\t__vrm_portal_id = info[18:24].hex()\n\treturn __vrm_portal_id\n\n\n# See VE.Can registers - public.docx for definition of this conversion\ndef convert_vreg_version_to_readable(version):\n\tdef str_to_arr(x, length):\n\t\ta = []\n\t\tfor i in range(0, len(x), length):\n\t\t\ta.append(x[i:i+length])\n\t\treturn a\n\n\tx = \"%x\" % version\n\tx = x.upper()\n\n\tif len(x) == 5 or len(x) == 3 or len(x) == 1:\n\t\tx = '0' + x\n\n\ta = str_to_arr(x, 2);\n\n\t# remove the first 00 if there are three bytes and it is 00\n\tif len(a) == 3 and a[0] == '00':\n\t\ta.remove(0);\n\n\t# if we have two or three bytes now, and the first character is a 0, remove it\n\tif len(a) >= 2 and a[0][0:1] == '0':\n\t\ta[0] = a[0][1];\n\n\tresult = ''\n\tfor item in a:\n\t\tresult += ('.' if result != '' else '') + item\n\n\n\tresult = 'v' + result\n\n\treturn result\n\n\ndef get_free_space(path):\n\tresult = -1\n\n\ttry:\n\t\ts = statvfs(path)\n\t\tresult = s.f_frsize * s.f_bavail     # Number of free bytes that ordinary users\n\texcept Exception as ex:\n\t\tlogger.info(\"Error while retrieving free space for path %s: %s\" % (path, ex))\n\n\treturn result\n\n\ndef _get_sysfs_machine_name():\n\ttry:\n\t\twith open('/sys/firmware/devicetree/base/model', 'r') as f:\n\t\t\treturn f.read().rstrip('\\x00')\n\texcept IOError:\n\t\tpass\n\n\treturn None\n\n# Returns None if it cannot find a machine name. Otherwise returns the string\n# containing the name\ndef get_machine_name():\n\t# First try calling the venus utility script\n\ttry:\n\t\treturn check_output(\"/usr/bin/product-name\").strip().decode('UTF-8')\n\texcept (CalledProcessError, OSError):\n\t\tpass\n\n\t# Fall back to sysfs\n\tname = _get_sysfs_machine_name()\n\tif name is not None:\n\t\treturn name\n\n\t# Fall back to venus build machine name\n\ttry:\n\t\twith open('/etc/venus/machine', 'r', encoding='UTF-8') as f:\n\t\t\treturn f.read().strip()\n\texcept IOError:\n\t\tpass\n\n\treturn None\n\n\ndef get_product_id():\n\t\"\"\" Find the machine ID and return it. \"\"\"\n\n\t# First try calling the venus utility script\n\ttry:\n\t\treturn check_output(\"/usr/bin/product-id\").strip().decode('UTF-8')\n\texcept (CalledProcessError, OSError):\n\t\tpass\n\n\t# Fall back machine name mechanism\n\tname = _get_sysfs_machine_name()\n\treturn {\n\t\t'Color Control GX': 'C001',\n\t\t'Venus GX': 'C002',\n\t\t'Octo GX': 'C006',\n\t\t'EasySolar-II': 'C007',\n\t\t'MultiPlus-II': 'C008',\n\t\t'Maxi GX': 'C009',\n\t\t'Cerbo GX': 'C00A'\n\t}.get(name, 'C003') # C003 is Generic\n\n\n# Returns False if it cannot open the file. Otherwise returns its rstripped contents\ndef read_file(path):\n\tcontent = False\n\n\ttry:\n\t\twith open(path, 'r') as f:\n\t\t\tcontent = f.read().rstrip()\n\texcept Exception as ex:\n\t\tlogger.debug(\"Error while reading %s: %s\" % (path, ex))\n\n\treturn content\n\n\ndef wrap_dbus_value(value):\n\tif value is None:\n\t\treturn VEDBUS_INVALID\n\tif isinstance(value, float):\n\t\treturn dbus.Double(value, variant_level=1)\n\tif isinstance(value, bool):\n\t\treturn dbus.Boolean(value, variant_level=1)\n\tif isinstance(value, int):\n\t\ttry:\n\t\t\treturn dbus.Int32(value, variant_level=1)\n\t\texcept OverflowError:\n\t\t\treturn dbus.Int64(value, variant_level=1)\n\tif isinstance(value, str):\n\t\treturn dbus.String(value, variant_level=1)\n\tif isinstance(value, list):\n\t\tif len(value) == 0:\n\t\t\t# If the list is empty we cannot infer the type of the contents. So assume unsigned integer.\n\t\t\t# A (signed) integer is dangerous, because an empty list of signed integers is used to encode\n\t\t\t# an invalid value.\n\t\t\treturn dbus.Array([], signature=dbus.Signature('u'), variant_level=1)\n\t\treturn dbus.Array([wrap_dbus_value(x) for x in value], variant_level=1)\n\tif isinstance(value, dict):\n\t\t# Wrapping the keys of the dictionary causes D-Bus errors like:\n\t\t# 'arguments to dbus_message_iter_open_container() were incorrect,\n\t\t# assertion \"(type == DBUS_TYPE_ARRAY && contained_signature &&\n\t\t# *contained_signature == DBUS_DICT_ENTRY_BEGIN_CHAR) || (contained_signature == NULL ||\n\t\t# _dbus_check_is_valid_signature (contained_signature))\" failed in file ...'\n\t\treturn dbus.Dictionary({(k, wrap_dbus_value(v)) for k, v in value.items()}, variant_level=1)\n\treturn value\n\n\ndbus_int_types = (dbus.Int32, dbus.UInt32, dbus.Byte, dbus.Int16, dbus.UInt16, dbus.UInt32, dbus.Int64, dbus.UInt64)\n\n\ndef unwrap_dbus_value(val):\n\t\"\"\"Converts D-Bus values back to the original type. For example if val is of type DBus.Double,\n\ta float will be returned.\"\"\"\n\tif isinstance(val, dbus_int_types):\n\t\treturn int(val)\n\tif isinstance(val, dbus.Double):\n\t\treturn float(val)\n\tif isinstance(val, dbus.Array):\n\t\tv = [unwrap_dbus_value(x) for x in val]\n\t\treturn None if len(v) == 0 else v\n\tif isinstance(val, (dbus.Signature, dbus.String)):\n\t\treturn str(val)\n\t# Python has no byte type, so we convert to an integer.\n\tif isinstance(val, dbus.Byte):\n\t\treturn int(val)\n\tif isinstance(val, dbus.ByteArray):\n\t\treturn \"\".join([bytes(x) for x in val])\n\tif isinstance(val, (list, tuple)):\n\t\treturn [unwrap_dbus_value(x) for x in val]\n\tif isinstance(val, (dbus.Dictionary, dict)):\n\t\t# Do not unwrap the keys, see comment in wrap_dbus_value\n\t\treturn dict([(x, unwrap_dbus_value(y)) for x, y in val.items()])\n\tif isinstance(val, dbus.Boolean):\n\t\treturn bool(val)\n\treturn val\n\n# When supported, only name owner changes for the the given namespace are reported. This\n# prevents spending cpu time at irrelevant changes, like scripts accessing the bus temporarily.\ndef add_name_owner_changed_receiver(dbus, name_owner_changed, namespace=\"com.victronenergy\"):\n\t# support for arg0namespace is submitted upstream, but not included at the time of\n\t# writing, Venus OS does support it, so try if it works.\n\tif namespace is None:\n\t\tdbus.add_signal_receiver(name_owner_changed, signal_name='NameOwnerChanged')\n\telse:\n\t\ttry:\n\t\t\tdbus.add_signal_receiver(name_owner_changed,\n\t\t\t\tsignal_name='NameOwnerChanged', arg0namespace=namespace)\n\t\texcept TypeError:\n\t\t\tdbus.add_signal_receiver(name_owner_changed, signal_name='NameOwnerChanged')\n"
  },
  {
    "path": "velib_python/velib_python/v3.41/vedbus.py",
    "content": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\nimport dbus.service\nimport logging\nimport traceback\nimport os\nimport weakref\nfrom collections import defaultdict\nfrom ve_utils import wrap_dbus_value, unwrap_dbus_value\n\n# vedbus contains three classes:\n# VeDbusItemImport -> use this to read data from the dbus, ie import\n# VeDbusItemExport -> use this to export data to the dbus (one value)\n# VeDbusService -> use that to create a service and export several values to the dbus\n\n# Code for VeDbusItemImport is copied from busitem.py and thereafter modified.\n# All projects that used busitem.py need to migrate to this package. And some\n# projects used to define there own equivalent of VeDbusItemExport. Better to\n# use VeDbusItemExport, or even better the VeDbusService class that does it all for you.\n\n# TODOS\n# 1 check for datatypes, it works now, but not sure if all is compliant with\n#\tcom.victronenergy.BusItem interface definition. See also the files in\n#\ttests_and_examples. And see 'if type(v) == dbus.Byte:' on line 102. Perhaps\n#\tsomething similar should also be done in VeDbusBusItemExport?\n# 2 Shouldn't VeDbusBusItemExport inherit dbus.service.Object?\n# 7 Make hard rules for services exporting data to the D-Bus, in order to make tracking\n#   changes possible. Does everybody first invalidate its data before leaving the bus?\n#   And what about before taking one object away from the bus, instead of taking the\n#   whole service offline?\n#   They should! And after taking one value away, do we need to know that someone left\n#   the bus? Or we just keep that value in invalidated for ever? Result is that we can't\n#   see the difference anymore between an invalidated value and a value that was first on\n#   the bus and later not anymore. See comments above VeDbusItemImport as well.\n# 9 there are probably more todos in the code below.\n\n# Some thoughts with regards to the data types:\n#\n#   Text from: http://dbus.freedesktop.org/doc/dbus-python/doc/tutorial.html#data-types\n#   ---\n#   Variants are represented by setting the variant_level keyword argument in the\n#   constructor of any D-Bus data type to a value greater than 0 (variant_level 1\n#   means a variant containing some other data type, variant_level 2 means a variant\n#   containing a variant containing some other data type, and so on). If a non-variant\n#   is passed as an argument but introspection indicates that a variant is expected,\n#   it'll automatically be wrapped in a variant.\n#   ---\n#\n#   Also the different dbus datatypes, such as dbus.Int32, and dbus.UInt32 are a subclass\n#   of Python int. dbus.String is a subclass of Python standard class unicode, etcetera\n#\n#   So all together that explains why we don't need to explicitly convert back and forth\n#   between the dbus datatypes and the standard python datatypes. Note that all datatypes\n#   in python are objects. Even an int is an object.\n\n#   The signature of a variant is 'v'.\n\n# Export ourselves as a D-Bus service.\nclass VeDbusService(object):\n\tdef __init__(self, servicename, bus=None, register=True):\n\t\t# dict containing the VeDbusItemExport objects, with their path as the key.\n\t\tself._dbusobjects = {}\n\t\tself._dbusnodes = {}\n\t\tself._ratelimiters = []\n\t\tself._dbusname = None\n\t\tself.name = servicename\n\n\t\t# dict containing the onchange callbacks, for each object. Object path is the key\n\t\tself._onchangecallbacks = {}\n\n\t\t# Connect to session bus whenever present, else use the system bus\n\t\tself._dbusconn = bus or (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else dbus.SystemBus())\n\n\t\t# make the dbus connection available to outside, could make this a true property instead, but ach..\n\t\tself.dbusconn = self._dbusconn\n\n\t\t# Add the root item that will return all items as a tree\n\t\tself._dbusnodes['/'] = VeDbusRootExport(self._dbusconn, '/', self)\n\n\t\t# Immediately register the service unless requested not to\n\t\tif register:\n\t\t\tself.register()\n\n\tdef register(self):\n\t\t# Register ourselves on the dbus, trigger an error if already in use (do_not_queue)\n\t\tself._dbusname = dbus.service.BusName(self.name, self._dbusconn, do_not_queue=True)\n\t\tlogging.info(\"registered ourselves on D-Bus as %s\" % self.name)\n\n\t# To force immediate deregistering of this dbus service and all its object paths, explicitly\n\t# call __del__().\n\tdef __del__(self):\n\t\tfor node in list(self._dbusnodes.values()):\n\t\t\tnode.__del__()\n\t\tself._dbusnodes.clear()\n\t\tfor item in list(self._dbusobjects.values()):\n\t\t\titem.__del__()\n\t\tself._dbusobjects.clear()\n\t\tif self._dbusname:\n\t\t\tself._dbusname.__del__()  # Forces call to self._bus.release_name(self._name), see source code\n\t\tself._dbusname = None\n\n\tdef get_name(self):\n\t\treturn self._dbusname.get_name()\n\n\t# @param callbackonchange\tfunction that will be called when this value is changed. First parameter will\n\t#\t\t\t\t\t\t\tbe the path of the object, second the new value. This callback should return\n\t#\t\t\t\t\t\t\tTrue to accept the change, False to reject it.\n\tdef add_path(self, path, value, description=\"\", writeable=False,\n\t\t\t\t\tonchangecallback=None, gettextcallback=None, valuetype=None, itemtype=None):\n\n\t\tif onchangecallback is not None:\n\t\t\tself._onchangecallbacks[path] = onchangecallback\n\n\t\titemtype = itemtype or VeDbusItemExport\n\t\titem = itemtype(self._dbusconn, path, value, description, writeable,\n\t\t\t\tself._value_changed, gettextcallback, deletecallback=self._item_deleted, valuetype=valuetype)\n\n\t\tspl = path.split('/')\n\t\tfor i in range(2, len(spl)):\n\t\t\tsubPath = '/'.join(spl[:i])\n\t\t\tif subPath not in self._dbusnodes and subPath not in self._dbusobjects:\n\t\t\t\tself._dbusnodes[subPath] = VeDbusTreeExport(self._dbusconn, subPath, self)\n\t\tself._dbusobjects[path] = item\n\t\tlogging.debug('added %s with start value %s. Writeable is %s' % (path, value, writeable))\n\t\treturn item\n\n\t# Add the mandatory paths, as per victron dbus api doc\n\tdef add_mandatory_paths(self, processname, processversion, connection,\n\t\t\tdeviceinstance, productid, productname, firmwareversion, hardwareversion, connected):\n\t\tself.add_path('/Mgmt/ProcessName', processname)\n\t\tself.add_path('/Mgmt/ProcessVersion', processversion)\n\t\tself.add_path('/Mgmt/Connection', connection)\n\n\t\t# Create rest of the mandatory objects\n\t\tself.add_path('/DeviceInstance', deviceinstance)\n\t\tself.add_path('/ProductId', productid)\n\t\tself.add_path('/ProductName', productname)\n\t\tself.add_path('/FirmwareVersion', firmwareversion)\n\t\tself.add_path('/HardwareVersion', hardwareversion)\n\t\tself.add_path('/Connected', connected)\n\n\t# Callback function that is called from the VeDbusItemExport objects when a value changes. This function\n\t# maps the change-request to the onchangecallback given to us for this specific path.\n\tdef _value_changed(self, path, newvalue):\n\t\tif path not in self._onchangecallbacks:\n\t\t\treturn True\n\n\t\treturn self._onchangecallbacks[path](path, newvalue)\n\n\tdef _item_deleted(self, path):\n\t\tself._dbusobjects.pop(path)\n\t\tfor np in list(self._dbusnodes.keys()):\n\t\t\tif np != '/':\n\t\t\t\tfor ip in self._dbusobjects:\n\t\t\t\t\tif ip.startswith(np + '/'):\n\t\t\t\t\t\tbreak\n\t\t\t\telse:\n\t\t\t\t\tself._dbusnodes[np].__del__()\n\t\t\t\t\tself._dbusnodes.pop(np)\n\n\tdef __getitem__(self, path):\n\t\treturn self._dbusobjects[path].local_get_value()\n\n\tdef __setitem__(self, path, newvalue):\n\t\tself._dbusobjects[path].local_set_value(newvalue)\n\n\tdef __delitem__(self, path):\n\t\tself._dbusobjects[path].__del__()  # Invalidates and then removes the object path\n\t\tassert path not in self._dbusobjects\n\n\tdef __contains__(self, path):\n\t\treturn path in self._dbusobjects\n\n\tdef __enter__(self):\n\t\tl = ServiceContext(self)\n\t\tself._ratelimiters.append(l)\n\t\treturn l\n\n\tdef __exit__(self, *exc):\n\t\t# pop off the top one and flush it. If with statements are nested\n\t\t# then each exit flushes its own part.\n\t\tif self._ratelimiters:\n\t\t\tself._ratelimiters.pop().flush()\n\nclass ServiceContext(object):\n\tdef __init__(self, parent):\n\t\tself.parent = parent\n\t\tself.changes = {}\n\n\tdef __contains__(self, path):\n\t\treturn path in self.parent\n\n\tdef __getitem__(self, path):\n\t\treturn self.parent[path]\n\n\tdef __setitem__(self, path, newvalue):\n\t\tc = self.parent._dbusobjects[path]._local_set_value(newvalue)\n\t\tif c is not None:\n\t\t\tself.changes[path] = c\n\n\tdef __delitem__(self, path):\n\t\tif path in self.changes:\n\t\t\tdel self.changes[path]\n\t\tdel self.parent[path]\n\n\tdef flush(self):\n\t\tif self.changes:\n\t\t\tself.parent._dbusnodes['/'].ItemsChanged(self.changes)\n\t\t\tself.changes.clear()\n\n\tdef add_path(self, path, value, *args, **kwargs):\n\t\tself.parent.add_path(path, value, *args, **kwargs)\n\t\tself.changes[path] = {\n\t\t\t'Value': wrap_dbus_value(value),\n\t\t\t'Text': self.parent._dbusobjects[path].GetText()\n\t\t}\n\n\tdef del_tree(self, root):\n\t\troot = root.rstrip('/')\n\t\tfor p in list(self.parent._dbusobjects.keys()):\n\t\t\tif p == root or p.startswith(root + '/'):\n\t\t\t\tself[p] = None\n\t\t\t\tself.parent._dbusobjects[p].__del__()\n\n\tdef get_name(self):\n\t\treturn self.parent.get_name()\n\nclass TrackerDict(defaultdict):\n\t\"\"\" Same as defaultdict, but passes the key to default_factory. \"\"\"\n\tdef __missing__(self, key):\n\t\tself[key] = x = self.default_factory(key)\n\t\treturn x\n\nclass VeDbusRootTracker(object):\n\t\"\"\" This tracks the root of a dbus path and listens for PropertiesChanged\n\t    signals. When a signal arrives, parse it and unpack the key/value changes\n\t    into traditional events, then pass it to the original eventCallback\n\t    method. \"\"\"\n\tdef __init__(self, bus, serviceName):\n\t\tself.importers = defaultdict(weakref.WeakSet)\n\t\tself.serviceName = serviceName\n\t\tself._match = bus.get_object(serviceName, '/', introspect=False).connect_to_signal(\n\t\t\t\"ItemsChanged\", weak_functor(self._items_changed_handler))\n\n\tdef __del__(self):\n\t\tself._match.remove()\n\t\tself._match = None\n\n\tdef add(self, i):\n\t\tself.importers[i.path].add(i)\n\n\tdef _items_changed_handler(self, items):\n\t\tif not isinstance(items, dict):\n\t\t\treturn\n\n\t\tfor path, changes in items.items():\n\t\t\ttry:\n\t\t\t\tv = changes['Value']\n\t\t\texcept KeyError:\n\t\t\t\tcontinue\n\n\t\t\ttry:\n\t\t\t\tt = changes['Text']\n\t\t\texcept KeyError:\n\t\t\t\tt = str(unwrap_dbus_value(v))\n\n\t\t\tfor i in self.importers.get(path, ()):\n\t\t\t\ti._properties_changed_handler({'Value': v, 'Text': t})\n\n\"\"\"\nImporting basics:\n\t- If when we power up, the D-Bus service does not exist, or it does exist and the path does not\n\t  yet exist, still subscribe to a signal: as soon as it comes online it will send a signal with its\n\t  initial value, which VeDbusItemImport will receive and use to update local cache. And, when set,\n\t  call the eventCallback.\n\t- If when we power up, save it\n\t- When using get_value, know that there is no difference between services (or object paths) that don't\n\t  exist and paths that are invalid (= empty array, see above). Both will return None. In case you do\n\t  really want to know ifa path exists or not, use the exists property.\n\t- When a D-Bus service leaves the D-Bus, it will first invalidate all its values, and send signals\n\t  with that update, and only then leave the D-Bus. (or do we need to subscribe to the NameOwnerChanged-\n\t  signal!?!) To be discussed and make sure. Not really urgent, since all existing code that uses this\n\t  class already subscribes to the NameOwnerChanged signal, and subsequently removes instances of this\n\t  class.\n\nRead when using this class:\nNote that when a service leaves that D-Bus without invalidating all its exported objects first, for\nexample because it is killed, VeDbusItemImport doesn't have a clue. So when using VeDbusItemImport,\nmake sure to also subscribe to the NamerOwnerChanged signal on bus-level. Or just use dbusmonitor,\nbecause that takes care of all of that for you.\n\"\"\"\nclass VeDbusItemImport(object):\n\tdef __new__(cls, bus, serviceName, path, eventCallback=None, createsignal=True):\n\t\tinstance = object.__new__(cls)\n\n\t\t# If signal tracking should be done, also add to root tracker\n\t\tif createsignal:\n\t\t\tif \"_roots\" not in cls.__dict__:\n\t\t\t\tcls._roots = TrackerDict(lambda k: VeDbusRootTracker(bus, k))\n\n\t\treturn instance\n\n\t## Constructor\n\t# @param bus\t\t\tthe bus-object (SESSION or SYSTEM).\n\t# @param serviceName\tthe dbus-service-name (string), for example 'com.victronenergy.battery.ttyO1'\n\t# @param path\t\t\tthe object-path, for example '/Dc/V'\n\t# @param eventCallback\tfunction that you want to be called on a value change\n\t# @param createSignal   only set this to False if you use this function to one time read a value. When\n\t#\t\t\t\t\t\tleaving it to True, make sure to also subscribe to the NameOwnerChanged signal\n\t#\t\t\t\t\t\telsewhere. See also note some 15 lines up.\n\tdef __init__(self, bus, serviceName, path, eventCallback=None, createsignal=True):\n\t\t# TODO: is it necessary to store _serviceName and _path? Isn't it\n\t\t# stored in the bus_getobjectsomewhere?\n\t\tself._serviceName = serviceName\n\t\tself._path = path\n\t\tself._match = None\n\t\t# TODO: _proxy is being used in settingsdevice.py, make a getter for that\n\t\tself._proxy = bus.get_object(serviceName, path, introspect=False)\n\t\tself.eventCallback = eventCallback\n\n\t\tassert eventCallback is None or createsignal == True\n\t\tif createsignal:\n\t\t\tself._match = self._proxy.connect_to_signal(\n\t\t\t\t\"PropertiesChanged\", weak_functor(self._properties_changed_handler))\n\t\t\tself._roots[serviceName].add(self)\n\n\t\t# store the current value in _cachedvalue. When it doesn't exists set _cachedvalue to\n\t\t# None, same as when a value is invalid\n\t\tself._cachedvalue = None\n\t\ttry:\n\t\t\tv = self._proxy.GetValue()\n\t\texcept dbus.exceptions.DBusException:\n\t\t\tpass\n\t\telse:\n\t\t\tself._cachedvalue = unwrap_dbus_value(v)\n\n\tdef __del__(self):\n\t\tif self._match is not None:\n\t\t\tself._match.remove()\n\t\t\tself._match = None\n\t\tself._proxy = None\n\n\tdef _refreshcachedvalue(self):\n\t\tself._cachedvalue = unwrap_dbus_value(self._proxy.GetValue())\n\n\t## Returns the path as a string, for example '/AC/L1/V'\n\t@property\n\tdef path(self):\n\t\treturn self._path\n\n\t## Returns the dbus service name as a string, for example com.victronenergy.vebus.ttyO1\n\t@property\n\tdef serviceName(self):\n\t\treturn self._serviceName\n\n\t## Returns the value of the dbus-item.\n\t# the type will be a dbus variant, for example dbus.Int32(0, variant_level=1)\n\t# this is not a property to keep the name consistant with the com.victronenergy.busitem interface\n\t# returns None when the property is invalid\n\tdef get_value(self):\n\t\treturn self._cachedvalue\n\n\t## Writes a new value to the dbus-item\n\tdef set_value(self, newvalue):\n\t\tr = self._proxy.SetValue(wrap_dbus_value(newvalue))\n\n\t\t# instead of just saving the value, go to the dbus and get it. So we have the right type etc.\n\t\tif r == 0:\n\t\t\tself._refreshcachedvalue()\n\n\t\treturn r\n\n\t## Resets the item to its default value\n\tdef set_default(self):\n\t\tself._proxy.SetDefault()\n\t\tself._refreshcachedvalue()\n\n\t## Returns the text representation of the value.\n\t# For example when the value is an enum/int GetText might return the string\n\t# belonging to that enum value. Another example, for a voltage, GetValue\n\t# would return a float, 12.0Volt, and GetText could return 12 VDC.\n\t#\n\t# Note that this depends on how the dbus-producer has implemented this.\n\tdef get_text(self):\n\t\treturn self._proxy.GetText()\n\n\t## Returns true of object path exists, and false if it doesn't\n\t@property\n\tdef exists(self):\n\t\t# TODO: do some real check instead of this crazy thing.\n\t\tr = False\n\t\ttry:\n\t\t\tr = self._proxy.GetValue()\n\t\t\tr = True\n\t\texcept dbus.exceptions.DBusException:\n\t\t\tpass\n\n\t\treturn r\n\n\t## callback for the trigger-event.\n\t# @param eventCallback the event-callback-function.\n\t@property\n\tdef eventCallback(self):\n\t\treturn self._eventCallback\n\n\t@eventCallback.setter\n\tdef eventCallback(self, eventCallback):\n\t\tself._eventCallback = eventCallback\n\n\t## Is called when the value of the imported bus-item changes.\n\t# Stores the new value in our local cache, and calls the eventCallback, if set.\n\tdef _properties_changed_handler(self, changes):\n\t\tif \"Value\" in changes:\n\t\t\tchanges['Value'] = unwrap_dbus_value(changes['Value'])\n\t\t\tself._cachedvalue = changes['Value']\n\t\t\tif self._eventCallback:\n\t\t\t\t# The reason behind this try/except is to prevent errors silently ending up the an error\n\t\t\t\t# handler in the dbus code.\n\t\t\t\ttry:\n\t\t\t\t\tself._eventCallback(self._serviceName, self._path, changes)\n\t\t\t\texcept:\n\t\t\t\t\ttraceback.print_exc()\n\t\t\t\t\tos._exit(1)  # sys.exit() is not used, since that also throws an exception\n\n\nclass VeDbusTreeExport(dbus.service.Object):\n\tdef __init__(self, bus, objectPath, service):\n\t\tdbus.service.Object.__init__(self, bus, objectPath)\n\t\tself._service = service\n\t\tlogging.debug(\"VeDbusTreeExport %s has been created\" % objectPath)\n\n\tdef __del__(self):\n\t\t# self._get_path() will raise an exception when retrieved after the call to .remove_from_connection,\n\t\t# so we need a copy.\n\t\tpath = self._get_path()\n\t\tif path is None:\n\t\t\treturn\n\t\tself.remove_from_connection()\n\t\tlogging.debug(\"VeDbusTreeExport %s has been removed\" % path)\n\n\tdef _get_path(self):\n\t\tif len(self._locations) == 0:\n\t\t\treturn None\n\t\treturn self._locations[0][1]\n\n\tdef _get_value_handler(self, path, get_text=False):\n\t\tlogging.debug(\"_get_value_handler called for %s\" % path)\n\t\tr = {}\n\t\tpx = path\n\t\tif not px.endswith('/'):\n\t\t\tpx += '/'\n\t\tfor p, item in self._service._dbusobjects.items():\n\t\t\tif p.startswith(px):\n\t\t\t\tv = item.GetText() if get_text else wrap_dbus_value(item.local_get_value())\n\t\t\t\tr[p[len(px):]] = v\n\t\tlogging.debug(r)\n\t\treturn r\n\n\t@dbus.service.method('com.victronenergy.BusItem', out_signature='v')\n\tdef GetValue(self):\n\t\tvalue = self._get_value_handler(self._get_path())\n\t\treturn dbus.Dictionary(value, signature=dbus.Signature('sv'), variant_level=1)\n\n\t@dbus.service.method('com.victronenergy.BusItem', out_signature='v')\n\tdef GetText(self):\n\t\treturn self._get_value_handler(self._get_path(), True)\n\n\tdef local_get_value(self):\n\t\treturn self._get_value_handler(self.path)\n\nclass VeDbusRootExport(VeDbusTreeExport):\n\t@dbus.service.signal('com.victronenergy.BusItem', signature='a{sa{sv}}')\n\tdef ItemsChanged(self, changes):\n\t\tpass\n\n\t@dbus.service.method('com.victronenergy.BusItem', out_signature='a{sa{sv}}')\n\tdef GetItems(self):\n\t\treturn {\n\t\t\tpath: {\n\t\t\t\t'Value': wrap_dbus_value(item.local_get_value()),\n\t\t\t\t'Text': item.GetText() }\n\t\t\tfor path, item in self._service._dbusobjects.items()\n\t\t}\n\n\nclass VeDbusItemExport(dbus.service.Object):\n\t## Constructor of VeDbusItemExport\n\t#\n\t# Use this object to export (publish), values on the dbus\n\t# Creates the dbus-object under the given dbus-service-name.\n\t# @param bus\t\t  The dbus object.\n\t# @param objectPath\t  The dbus-object-path.\n\t# @param value\t\t  Value to initialize ourselves with, defaults to None which means Invalid\n\t# @param description  String containing a description. Can be called over the dbus with GetDescription()\n\t# @param writeable\t  what would this do!? :).\n\t# @param callback\t  Function that will be called when someone else changes the value of this VeBusItem\n\t#                     over the dbus. First parameter passed to callback will be our path, second the new\n\t#\t\t\t\t\t  value. This callback should return True to accept the change, False to reject it.\n\tdef __init__(self, bus, objectPath, value=None, description=None, writeable=False,\n\t\t\t\t\tonchangecallback=None, gettextcallback=None, deletecallback=None,\n\t\t\t\t\tvaluetype=None):\n\t\tdbus.service.Object.__init__(self, bus, objectPath)\n\t\tself._onchangecallback = onchangecallback\n\t\tself._gettextcallback = gettextcallback\n\t\tself._value = value\n\t\tself._description = description\n\t\tself._writeable = writeable\n\t\tself._deletecallback = deletecallback\n\t\tself._type = valuetype\n\n\t# To force immediate deregistering of this dbus object, explicitly call __del__().\n\tdef __del__(self):\n\t\t# self._get_path() will raise an exception when retrieved after the\n\t\t# call to .remove_from_connection, so we need a copy.\n\t\tpath = self._get_path()\n\t\tif path == None:\n\t\t\treturn\n\t\tif self._deletecallback is not None:\n\t\t\tself._deletecallback(path)\n\t\tself.remove_from_connection()\n\t\tlogging.debug(\"VeDbusItemExport %s has been removed\" % path)\n\n\tdef _get_path(self):\n\t\tif len(self._locations) == 0:\n\t\t\treturn None\n\t\treturn self._locations[0][1]\n\n\t## Sets the value. And in case the value is different from what it was, a signal\n\t# will be emitted to the dbus. This function is to be used in the python code that\n\t# is using this class to export values to the dbus.\n\t# set value to None to indicate that it is Invalid\n\tdef local_set_value(self, newvalue):\n\t\tchanges = self._local_set_value(newvalue)\n\t\tif changes is not None:\n\t\t\tself.PropertiesChanged(changes)\n\n\tdef _local_set_value(self, newvalue):\n\t\tif self._value == newvalue:\n\t\t\treturn None\n\n\t\tself._value = newvalue\n\t\treturn {\n\t\t\t'Value': wrap_dbus_value(newvalue),\n\t\t\t'Text': self.GetText()\n\t\t}\n\n\tdef local_get_value(self):\n\t\treturn self._value\n\n\t# ==== ALL FUNCTIONS BELOW THIS LINE WILL BE CALLED BY OTHER PROCESSES OVER THE DBUS ====\n\n\t## Dbus exported method SetValue\n\t# Function is called over the D-Bus by other process. It will first check (via callback) if new\n\t# value is accepted. And it is, stores it and emits a changed-signal.\n\t# @param value The new value.\n\t# @return completion-code When successful a 0 is return, and when not a -1 is returned.\n\t@dbus.service.method('com.victronenergy.BusItem', in_signature='v', out_signature='i')\n\tdef SetValue(self, newvalue):\n\t\tif not self._writeable:\n\t\t\treturn 1  # NOT OK\n\n\t\tnewvalue = unwrap_dbus_value(newvalue)\n\n\t\t# If value type is enforced, cast it. If the type can be coerced\n\t\t# python will do it for us. This allows ints to become floats,\n\t\t# or bools to become ints. Additionally also allow None, so that\n\t\t# a path may be invalidated.\n\t\tif self._type is not None and newvalue is not None:\n\t\t\ttry:\n\t\t\t\tnewvalue = self._type(newvalue)\n\t\t\texcept (ValueError, TypeError):\n\t\t\t\treturn 1 # NOT OK\n\n\t\tif newvalue == self._value:\n\t\t\treturn 0  # OK\n\n\t\t# call the callback given to us, and check if new value is OK.\n\t\tif (self._onchangecallback is None or\n\t\t\t\t(self._onchangecallback is not None and self._onchangecallback(self.__dbus_object_path__, newvalue))):\n\n\t\t\tself.local_set_value(newvalue)\n\t\t\treturn 0  # OK\n\n\t\treturn 2  # NOT OK\n\n\t## Dbus exported method GetDescription\n\t#\n\t# Returns the a description.\n\t# @param language A language code (e.g. ISO 639-1 en-US).\n\t# @param length Lenght of the language string.\n\t# @return description\n\t@dbus.service.method('com.victronenergy.BusItem', in_signature='si', out_signature='s')\n\tdef GetDescription(self, language, length):\n\t\treturn self._description if self._description is not None else 'No description given'\n\n\t## Dbus exported method GetValue\n\t# Returns the value.\n\t# @return the value when valid, and otherwise an empty array\n\t@dbus.service.method('com.victronenergy.BusItem', out_signature='v')\n\tdef GetValue(self):\n\t\treturn wrap_dbus_value(self._value)\n\n\t## Dbus exported method GetText\n\t# Returns the value as string of the dbus-object-path.\n\t# @return text A text-value. '---' when local value is invalid\n\t@dbus.service.method('com.victronenergy.BusItem', out_signature='s')\n\tdef GetText(self):\n\t\tif self._value is None:\n\t\t\treturn '---'\n\n\t\t# Default conversion from dbus.Byte will get you a character (so 'T' instead of '84'), so we\n\t\t# have to convert to int first. Note that if a dbus.Byte turns up here, it must have come from\n\t\t# the application itself, as all data from the D-Bus should have been unwrapped by now.\n\t\tif self._gettextcallback is None and type(self._value) == dbus.Byte:\n\t\t\treturn str(int(self._value))\n\n\t\tif self._gettextcallback is None and self.__dbus_object_path__ == '/ProductId':\n\t\t\treturn \"0x%X\" % self._value\n\n\t\tif self._gettextcallback is None:\n\t\t\treturn str(self._value)\n\n\t\treturn self._gettextcallback(self.__dbus_object_path__, self._value)\n\n\t## The signal that indicates that the value has changed.\n\t# Other processes connected to this BusItem object will have subscribed to the\n\t# event when they want to track our state.\n\t@dbus.service.signal('com.victronenergy.BusItem', signature='a{sv}')\n\tdef PropertiesChanged(self, changes):\n\t\tpass\n\n## This class behaves like a regular reference to a class method (eg. self.foo), but keeps a weak reference\n## to the object which method is to be called.\n## Use this object to break circular references.\nclass weak_functor:\n\tdef __init__(self, f):\n\t\tself._r = weakref.ref(f.__self__)\n\t\tself._f = weakref.ref(f.__func__)\n\n\tdef __call__(self, *args, **kargs):\n\t\tr = self._r()\n\t\tf = self._f()\n\t\tif r == None or f == None:\n\t\t\treturn\n\t\tf(r, *args, **kargs)\n"
  },
  {
    "path": "version",
    "content": "v9.4\n"
  }
]