├── .flake8 ├── .gitattributes ├── .gitignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── dbus-multiplus-emulator ├── config.sample.ini ├── dbus-multiplus-emulator.py ├── ext │ └── velib_python │ │ ├── LICENSE │ │ ├── README.md │ │ ├── dbusmonitor.py │ │ ├── ve_utils.py │ │ └── vedbus.py ├── install.sh ├── restart.sh ├── service │ ├── log │ │ └── run │ └── run └── uninstall.sh ├── download.sh └── pyproject.toml /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 216 3 | exclude = 4 | ./dbus-multiplus-emulator/ext 5 | extend-ignore: 6 | # E203 whitespace before ':' conflicts with black code formatting. Will be ignored in flake8 7 | E203 8 | # E402 module level import not at top of file 9 | E402 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behaviour, in case users don't have core.autocrlf set. 2 | text eol=lf 3 | 4 | # Explicitly declare text files you want to always be normalized and converted 5 | # to native line endings on checkout. 6 | *.ini text 7 | *.md text 8 | *.py text 9 | *.sh text 10 | run text 11 | 12 | # Denote all files that are truly binary and should not be modified. 13 | *.gif binary 14 | *.jpg binary 15 | *.png binary 16 | *.zip binary -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /*/config.ini 2 | /data 3 | /logs 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[python]": { "editor.formatOnSave": true }, 3 | "python.analysis.extraPaths": [ 4 | "./dbus-multiplus-emulator/ext/velib_python" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v1.0.0 4 | * Added: Calculate ratio between phases based on grid and PV inverter 5 | * Added: Config file for more convinient settings changes 6 | * Added: Specify an AC Load Meter to get correct inverter data 7 | * Changed: Fixed wrong calculations 8 | * Changed: Rewrote the whole driver 9 | 10 | ## v0.1.0 11 | * Added: Support for three phases 12 | * Added: Some fields 13 | 14 | ## v0.0.3 15 | * Added: Energy sum of power from `Out to Inverter` and `Inverter to Out` 16 | * Added: LED display 17 | * Added: Units to the values 18 | 19 | ## v0.0.2 20 | * Added: Get automatically the grid and battery meter, if there is only one 21 | * Added: Select on which phase the PV Inverter is connected to 22 | * Changed: Fixed caluclations for AC-Out 23 | 24 | ## v0.0.1 25 | Initial release 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Manuel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## dbus-mutliplus-emulator - Emulates a MultiPlus II 48/5000/70-50 2 | 3 | GitHub repository: [mr-manuel/venus-os_dbus-multiplus-emulator](https://github.com/mr-manuel/venus-os_dbus-multiplus-emulator) 4 | 5 | ## Index 6 | 7 | 1. [Disclaimer](#disclaimer) 8 | 1. [Supporting/Sponsoring this project](#supportingsponsoring-this-project) 9 | 1. [Purpose](#purpose) 10 | 1. [Config](#config) 11 | 1. [Install / Update](#install--update) 12 | 1. [Uninstall](#uninstall) 13 | 1. [Restart](#restart) 14 | 1. [Debugging](#debugging) 15 | 1. [Compatibility](#compatibility) 16 | 17 | 18 | ## Disclaimer 19 | 20 | I wrote this script for myself. I'm not responsible, if you damage something using my script. 21 | 22 | ## Supporting/Sponsoring this project 23 | 24 | You like the project and you want to support me? 25 | 26 | [](https://www.paypal.com/donate/?hosted_button_id=3NEVZBDM5KABW) 27 | 28 | 29 | ## Purpose 30 | The script emulates a MultiPlus II in Venus OS. This allows to show the correct values in the overview. 31 | 32 | ## Config 33 | There is nothing specific to configure and it should work out of the box for systems that have only `L1`. If you have multiple phases, grid meters and/or batteries, then a configuration is maybe needed. In this case edit the `dbus-multiplus-emulator.py` and search for the `USER CHANGABLE VALUES | START` section. 34 | 35 | In a multi-phase system, the DC loads are distributed based on the combined power from each phase of the grid and PV inverters. To achieve more accurate readings, you need to provide the power going in and out of the charger/inverter on the AC side. You can then use the [`dbus-mqtt-grid`](https://github.com/mr-manuel/venus-os_dbus-mqtt-grid) driver and configure it as an AC load to input these values into the emulator. 36 | 37 | ⚠️ Please note that the `AC Loads` value may not exactly match the actual values, because losses are included as part of the load. 38 | 39 | 40 | ## Install / Update 41 | 42 | 1. Login to your Venus OS device via SSH. See [Venus OS:Root Access](https://www.victronenergy.com/live/ccgx:root_access#root_access) for more details. 43 | 44 | 2. Execute this commands to download and copy the files: 45 | 46 | ```bash 47 | wget -O /tmp/download_dbus-multiplus-emulator.sh https://raw.githubusercontent.com/mr-manuel/venus-os_dbus-multiplus-emulator/master/download.sh 48 | 49 | bash /tmp/download_dbus-multiplus-emulator.sh 50 | ``` 51 | 52 | 3. Select the version you want to install. 53 | 54 | ### Extra steps for your first installation 55 | 56 | 4. Edit the config file if you have a multi-phase system or if you want to have a custom configuration: 57 | 58 | ```bash 59 | nano /data/etc/dbus-multiplus-emulator-2/config.ini 60 | ``` 61 | 62 | Otherwise, skip this step. 63 | 64 | 5. Install the driver as a service: 65 | 66 | ```bash 67 | bash /data/etc/dbus-multiplus-emulator/install.sh 68 | ``` 69 | 70 | The daemon-tools should start this service automatically within seconds. 71 | 72 | ## Uninstall 73 | 74 | Run `/data/etc/dbus-multiplus-emulator/uninstall.sh` 75 | 76 | ## Restart 77 | 78 | Run `/data/etc/dbus-multiplus-emulator/restart.sh` 79 | 80 | ## Debugging 81 | 82 | The service status can be checked with svstat `svstat /service/dbus-multiplus-emulator` 83 | 84 | This will output somethink like `/service/dbus-multiplus-emulator: up (pid 5845) 185 seconds` 85 | 86 | If the seconds are under 5 then the service crashes and gets restarted all the time. If you do not see anything in the logs you can increase the log level in `/data/etc/dbus-multiplus-emulator/dbus-multiplus-emulator.py` by changing `level=logging.WARNING` to `level=logging.INFO` or `level=logging.DEBUG` 87 | 88 | If the script stops with the message `dbus.exceptions.NameExistsException: Bus name already exists: com.victronenergy.grid.mqtt_grid"` it means that the service is still running or another service is using that bus name. 89 | 90 | ## Compatibility 91 | 92 | This software supports the latest three stable versions of Venus OS. It may also work on older versions, but this is not guaranteed. 93 | -------------------------------------------------------------------------------- /dbus-multiplus-emulator/config.sample.ini: -------------------------------------------------------------------------------- 1 | ; CONFIG FILE 2 | ; GitHub reporitory: https://github.com/mr-manuel/venus-os_dbus-multiplus-emulator 3 | ; remove semicolon ; to enable desired setting 4 | 5 | [DEFAULT] 6 | ; Set logging level 7 | ; ERROR = shows errors only 8 | ; WARNING = shows ERROR and warnings 9 | ; INFO = shows WARNING and running functions 10 | ; DEBUG = shows INFO and data/values 11 | ; default: WARNING 12 | logging = WARNING 13 | 14 | ; Device name 15 | ; default: MultiPlus-II xx/5000/xx-xx (emulated) 16 | device_name = MultiPlus-II xx/5000/xx-xx (emulated) 17 | 18 | ; uncomment or change the phase combination you are using 19 | ; default: L1 20 | phase_used = L1 21 | ; phase_used = L1, L2 22 | ; phase_used = L1, L2, L3 23 | 24 | ; enter the maximum power of the inverter of a single phase 25 | inverter_max_power = 4500 26 | 27 | ; enter the dbus service name from which the grid meter data should be fetched, if there is more than one 28 | ; e.g. com.victronenergy.grid.mqtt_grid_31 29 | dbus_service_name_grid = 30 | 31 | ; if there is more then one phase, the emulator can only divide the DC power equally to all AC phases 32 | ; since in reality this is rarely the case, it's possible to set an ac load meter which provides the power of each inverter per phase 33 | ; enter the dbus service name from which the ac load meter data should be fetched, if there is more than one 34 | ; e.g. com.victronenergy.acload.mqtt_acload_31 35 | dbus_service_name_ac_load = 36 | 37 | ; enter grid frequency 38 | ; used if the grid meter is not available or does not provide the frequency 39 | ; Europe 40 | grid_frequency = 50 41 | ; UK/USA 42 | ; grid_frequency = 60 43 | 44 | ; enter grid nominal voltage 45 | ; used if the grid meter is not available or does not provide the voltage 46 | ; Europe 47 | grid_nominal_voltage = 230 48 | ; UK/USA 49 | ; grid_nominal_voltage = 120 50 | -------------------------------------------------------------------------------- /dbus-multiplus-emulator/dbus-multiplus-emulator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import platform 4 | import logging 5 | import sys 6 | import os 7 | import _thread 8 | from time import sleep, time 9 | from typing import Union 10 | import json 11 | import configparser # for config/ini file 12 | 13 | import dbus 14 | from gi.repository import GLib 15 | from dbus.mainloop.glib import DBusGMainLoop 16 | 17 | 18 | # import Victron Energy packages 19 | sys.path.insert(1, os.path.join(os.path.dirname(__file__), "ext", "velib_python")) 20 | from vedbus import VeDbusService 21 | from vedbus import VeDbusItemImport 22 | 23 | 24 | # get values from config.ini file 25 | try: 26 | config_file = (os.path.dirname(os.path.realpath(__file__))) + "/config.ini" 27 | if os.path.exists(config_file): 28 | config = configparser.ConfigParser() 29 | config.read(config_file) 30 | else: 31 | print('ERROR:The "' + config_file + '" is not found. Did you copy or rename the "config.sample.ini" to "config.ini"? The driver restarts in 60 seconds.') 32 | sleep(60) 33 | sys.exit() 34 | 35 | except Exception: 36 | exception_type, exception_object, exception_traceback = sys.exc_info() 37 | file = exception_traceback.tb_frame.f_code.co_filename 38 | line = exception_traceback.tb_lineno 39 | print(f"Exception occurred: {repr(exception_object)} of type {exception_type} in {file} line #{line}") 40 | print("ERROR:The driver restarts in 60 seconds.") 41 | sleep(60) 42 | sys.exit() 43 | 44 | 45 | # Get logging level from config.ini 46 | # ERROR = shows errors only 47 | # WARNING = shows ERROR and warnings 48 | # INFO = shows WARNING and running functions 49 | # DEBUG = shows INFO and data/values 50 | if "DEFAULT" in config and "logging" in config["DEFAULT"]: 51 | if config["DEFAULT"]["logging"] == "DEBUG": 52 | logging.basicConfig(level=logging.DEBUG) 53 | elif config["DEFAULT"]["logging"] == "INFO": 54 | logging.basicConfig(level=logging.INFO) 55 | elif config["DEFAULT"]["logging"] == "ERROR": 56 | logging.basicConfig(level=logging.ERROR) 57 | else: 58 | logging.basicConfig(level=logging.WARNING) 59 | else: 60 | logging.basicConfig(level=logging.WARNING) 61 | 62 | # get values from config.ini file 63 | phase_used = config["DEFAULT"]["phase_used"].replace(" ", "").split(",") 64 | 65 | inverter_max_power = int(config["DEFAULT"]["inverter_max_power"]) 66 | dbus_service_name_grid = config["DEFAULT"]["dbus_service_name_grid"] 67 | dbus_service_name_ac_load = config["DEFAULT"]["dbus_service_name_ac_load"] 68 | grid_frequency = int(config["DEFAULT"]["grid_frequency"]) 69 | grid_nominal_voltage = int(config["DEFAULT"]["grid_nominal_voltage"]) 70 | 71 | 72 | # check if the phase_used list is valid 73 | valid_phases = {"L1", "L2", "L3"} 74 | for phase in phase_used: 75 | if phase not in valid_phases: 76 | logging.error(f"Invalid phase {phase} in phase_used list. Valid phases are {valid_phases}.") 77 | sleep(60) 78 | sys.exit() 79 | 80 | 81 | # specify how many phases are connected 82 | phase_count = len(phase_used) 83 | # create dictionary for later to count watt hours 84 | data_watt_hours = {"time_creation": int(time()), "count": 0} 85 | # calculate and save watthours after every x seconds 86 | data_watt_hours_timespan = 60 87 | # save file to non volatile storage after x seconds 88 | data_watt_hours_save = 900 89 | # file to save watt hours on persistent storage 90 | data_watt_hours_storage_file = "/data/etc/dbus-multiplus-emulator/data_watt_hours.json" 91 | # file to save many writing operations (best on ramdisk to not wear SD card) 92 | data_watt_hours_working_file = "/var/volatile/tmp/dbus-multiplus-emulator_data_watt_hours.json" 93 | # get last modification timestamp 94 | timestamp_storage_file = os.path.getmtime(data_watt_hours_storage_file) if os.path.isfile(data_watt_hours_storage_file) else 0 95 | 96 | # load data to prevent sending 0 watthours for OutToInverter (charging)/InverterToOut (discharging) before the first loop 97 | # check if file in volatile storage exists 98 | if os.path.isfile(data_watt_hours_working_file): 99 | with open(data_watt_hours_working_file, "r") as file: 100 | file = open(data_watt_hours_working_file, "r") 101 | json_data = json.load(file) 102 | logging.info("Loaded JSON for OutToInverter (charging)/InverterToOut (discharging) once") 103 | logging.debug(json.dumps(json_data)) 104 | # if not, check if file in persistent storage exists 105 | elif os.path.isfile(data_watt_hours_storage_file): 106 | with open(data_watt_hours_storage_file, "r") as file: 107 | file = open(data_watt_hours_storage_file, "r") 108 | json_data = json.load(file) 109 | logging.info("Loaded JSON for OutToInverter (charging)/InverterToOut (discharging) once from persistent storage") 110 | logging.debug(json.dumps(json_data)) 111 | else: 112 | json_data = {} 113 | 114 | 115 | class DbusMultiPlusEmulator: 116 | def __init__( 117 | self, 118 | servicename, 119 | deviceinstance, 120 | paths, 121 | productname=(config["DEFAULT"]["device_name"]), 122 | connection="VE.Bus", 123 | ): 124 | self._dbusservice = VeDbusService(servicename, register=False) 125 | self._paths = paths 126 | 127 | logging.debug("%s /DeviceInstance = %d" % (servicename, deviceinstance)) 128 | 129 | # Create the management objects, as specified in the ccgx dbus-api document 130 | self._dbusservice.add_path("/Mgmt/ProcessName", __file__) 131 | self._dbusservice.add_path( 132 | "/Mgmt/ProcessVersion", 133 | "Unkown version, and running on Python " + platform.python_version(), 134 | ) 135 | self._dbusservice.add_path("/Mgmt/Connection", connection) 136 | 137 | # Create the mandatory objects 138 | self._dbusservice.add_path("/DeviceInstance", deviceinstance) 139 | self._dbusservice.add_path("/ProductId", 2623) 140 | self._dbusservice.add_path("/ProductName", productname) 141 | self._dbusservice.add_path("/CustomName", "") 142 | self._dbusservice.add_path("/FirmwareVersion", 1296) 143 | self._dbusservice.add_path("/HardwareVersion", "1.0.0-beta1 (20241029)") 144 | self._dbusservice.add_path("/Connected", 1) 145 | 146 | # self._dbusservice.add_path('/Latency', None) 147 | # self._dbusservice.add_path('/ErrorCode', 0) 148 | # self._dbusservice.add_path('/Position', 0) 149 | # self._dbusservice.add_path('/StatusCode', 0) 150 | 151 | self._dbusservice.add_path("/Ac/ActiveIn/CurrentLimit", 50.0, writeable=True) 152 | 153 | for path, settings in self._paths.items(): 154 | self._dbusservice.add_path( 155 | path, 156 | settings["initial"], 157 | gettextcallback=settings["textformat"], 158 | writeable=True, 159 | # onchangecallback=self._handlechangedvalue, 160 | ) 161 | 162 | # create empty dictionaries for later use 163 | self.system_items = {} 164 | self.grid_items = {} 165 | self.ac_load_items = {} 166 | 167 | logging.info("-- Initializing completed, starting the main loop") 168 | 169 | # register VeDbusService after all paths where added 170 | self._dbusservice.register() 171 | 172 | GLib.timeout_add(1000, self._update) # pause 1000ms before the next request 173 | 174 | def zeroIfNone(self, value: Union[int, float, None]) -> float: 175 | """ 176 | Returns the value if it is not None, otherwise 0. 177 | """ 178 | return value if value is not None else 0 179 | 180 | def _update(self): 181 | global data_watt_hours, data_watt_hours_timespan, data_watt_hours_save 182 | global data_watt_hours_storage_file, data_watt_hours_working_file, json_data, timestamp_storage_file 183 | 184 | # ################################################################################################################## 185 | 186 | # check for changes in the dbus service list 187 | # if int(time()) % 15 == 0 or self.system_items == {}: 188 | # start = time() 189 | # self.system_items, self.grid_items, self.ac_load_items = setup_dbus_external_items() 190 | # logging.info("Time to setup external dbus items: %s seconds" % (time() - start)) 191 | 192 | # get DC values 193 | dc_power = self.zeroIfNone(self.system_items["/Dc/Battery/Power"].get_value()) 194 | dc_voltage = self.zeroIfNone(self.system_items["/Dc/Battery/Voltage"].get_value()) 195 | dc_current = self.zeroIfNone(self.system_items["/Dc/Battery/Current"].get_value()) 196 | 197 | # # # calculate watthours 198 | # measure power and calculate watthours, since it provides only watthours for production/import/consumption and no export 199 | # divide charging and discharging from dc 200 | # charging (+) 201 | dc_power_charging = dc_power if dc_power > 0 else 0 202 | # discharging (-) 203 | dc_power_discharging = dc_power * -1 if dc_power < 0 else 0 204 | 205 | # timestamp 206 | timestamp = int(time()) 207 | 208 | # sum up values for consumption calculation 209 | data_watt_hours_dc = { 210 | "charging": round( 211 | (data_watt_hours["dc"]["charging"] + dc_power_charging if "dc" in data_watt_hours else dc_power_charging), 212 | 3, 213 | ), 214 | "discharging": round( 215 | (data_watt_hours["dc"]["discharging"] + dc_power_discharging if "dc" in data_watt_hours else dc_power_discharging), 216 | 3, 217 | ), 218 | } 219 | 220 | data_watt_hours.update( 221 | { 222 | "dc": data_watt_hours_dc, 223 | "count": data_watt_hours["count"] + 1, 224 | } 225 | ) 226 | 227 | logging.debug("--> data_watt_hours(): %s" % json.dumps(data_watt_hours)) 228 | 229 | # build mean, calculate time diff and Wh and write to file 230 | # check if at least x seconds are passed 231 | if data_watt_hours["time_creation"] + data_watt_hours_timespan < timestamp: 232 | # check if file in volatile storage exists 233 | if os.path.isfile(data_watt_hours_working_file): 234 | with open(data_watt_hours_working_file, "r") as file: 235 | file = open(data_watt_hours_working_file, "r") 236 | data_watt_hours_old = json.load(file) 237 | logging.debug("Loaded JSON") 238 | logging.debug(json.dumps(data_watt_hours_old)) 239 | 240 | # if not, check if file in persistent storage exists 241 | elif os.path.isfile(data_watt_hours_storage_file): 242 | with open(data_watt_hours_storage_file, "r") as file: 243 | file = open(data_watt_hours_storage_file, "r") 244 | data_watt_hours_old = json.load(file) 245 | logging.debug("Loaded JSON from persistent storage") 246 | logging.debug(json.dumps(data_watt_hours_old)) 247 | 248 | # if not, generate data 249 | else: 250 | data_watt_hours_old_dc = { 251 | "charging": 0, 252 | "discharging": 0, 253 | } 254 | data_watt_hours_old = {"dc": data_watt_hours_old_dc} 255 | logging.debug("Generated JSON") 256 | logging.debug(json.dumps(data_watt_hours_old)) 257 | 258 | # factor to calculate Watthours: mean power * measuuring period / 3600 seconds (1 hour) 259 | factor = (timestamp - data_watt_hours["time_creation"]) / 3600 260 | 261 | dc_charging = round( 262 | data_watt_hours_old["dc"]["charging"] + (data_watt_hours["dc"]["charging"] / data_watt_hours["count"] * factor) / 1000, 263 | 3, 264 | ) 265 | dc_discharging = round( 266 | data_watt_hours_old["dc"]["discharging"] + (data_watt_hours["dc"]["discharging"] / data_watt_hours["count"] * factor) / 1000, 267 | 3, 268 | ) 269 | 270 | # update previously set data 271 | json_data = { 272 | "dc": { 273 | "charging": dc_charging, 274 | "discharging": dc_discharging, 275 | } 276 | } 277 | 278 | # save data to volatile storage 279 | with open(data_watt_hours_working_file, "w") as file: 280 | file.write(json.dumps(json_data)) 281 | 282 | # save data to persistent storage if time is passed 283 | if timestamp_storage_file + data_watt_hours_save < timestamp: 284 | with open(data_watt_hours_storage_file, "w") as file: 285 | file.write(json.dumps(json_data)) 286 | timestamp_storage_file = timestamp 287 | logging.info("Written JSON for OutToInverter (charging)/InverterToOut (discharging) to persistent storage.") 288 | 289 | # begin a new cycle 290 | data_watt_hours_dc = { 291 | "charging": round(dc_power_charging, 3), 292 | "discharging": round(dc_power_discharging, 3), 293 | } 294 | 295 | data_watt_hours = { 296 | "time_creation": timestamp, 297 | "dc": data_watt_hours_dc, 298 | "count": 1, 299 | } 300 | 301 | logging.debug("--> data_watt_hours(): %s" % json.dumps(data_watt_hours)) 302 | 303 | # update values in dbus 304 | # for bubble flow in chart and load visualization 305 | 306 | if self.ac_load_items != {}: 307 | # L1 ---- 308 | if "L1" in phase_used and self.ac_load_items["/Ac/L1/Power"] is not None: 309 | # power 310 | self._dbusservice["/Ac/ActiveIn/L1/P"] = self.ac_load_items["/Ac/L1/Power"].get_value() 311 | self._dbusservice["/Ac/ActiveIn/L1/S"] = self._dbusservice["/Ac/ActiveIn/L1/P"] 312 | 313 | # frequency 314 | if self.ac_load_items["/Ac/L1/Frequency"] is not None: 315 | self._dbusservice["/Ac/ActiveIn/L1/F"] = self.ac_load_items["/Ac/L1/Frequency"].get_value() 316 | elif self.grid_items != {} and self.grid_items["/Ac/L1/Frequency"] is not None: 317 | self._dbusservice["/Ac/ActiveIn/L1/F"] = self.ac_load_items["/Ac/L1/Frequency"].get_value() 318 | else: 319 | self._dbusservice["/Ac/ActiveIn/L1/F"] = grid_frequency 320 | 321 | # voltage 322 | if self.ac_load_items["/Ac/L1/Voltage"] is not None: 323 | self._dbusservice["/Ac/ActiveIn/L1/V"] = self.ac_load_items["/Ac/L1/Voltage"].get_value() 324 | elif self.grid_items != {} and self.grid_items["/Ac/L1/Voltage"] is not None: 325 | self._dbusservice["/Ac/ActiveIn/L1/V"] = self.grid_items["/Ac/L1/Voltage"].get_value() 326 | else: 327 | self._dbusservice["/Ac/ActiveIn/L1/V"] = grid_nominal_voltage 328 | 329 | # current 330 | if self.ac_load_items["/Ac/L1/Current"] is not None: 331 | self._dbusservice["/Ac/ActiveIn/L1/I"] = self.ac_load_items["/Ac/L1/Current"].get_value() 332 | else: 333 | self._dbusservice["/Ac/ActiveIn/L1/I"] = round(self._dbusservice["/Ac/ActiveIn/L1/P"] / self._dbusservice["/Ac/ActiveIn/L1/V"], 2) 334 | 335 | # L2 ---- 336 | if "L2" in phase_used and self.ac_load_items["/Ac/L2/Power"] is not None: 337 | # power 338 | self._dbusservice["/Ac/ActiveIn/L2/P"] = self.ac_load_items["/Ac/L2/Power"].get_value() 339 | self._dbusservice["/Ac/ActiveIn/L2/S"] = self._dbusservice["/Ac/ActiveIn/L2/P"] 340 | 341 | # frequency 342 | if self.ac_load_items["/Ac/L2/Frequency"] is not None: 343 | self._dbusservice["/Ac/ActiveIn/L2/F"] = self.ac_load_items["/Ac/L2/Frequency"].get_value() 344 | elif self.grid_items != {} and self.grid_items["/Ac/L2/Frequency"] is not None: 345 | self._dbusservice["/Ac/ActiveIn/L2/F"] = self.ac_load_items["/Ac/L2/Frequency"].get_value() 346 | else: 347 | self._dbusservice["/Ac/ActiveIn/L2/F"] = grid_frequency 348 | 349 | # voltage 350 | if self.ac_load_items["/Ac/L2/Voltage"] is not None: 351 | self._dbusservice["/Ac/ActiveIn/L2/V"] = self.ac_load_items["/Ac/L2/Voltage"].get_value() 352 | elif self.grid_items != {} and self.grid_items["/Ac/L2/Voltage"] is not None: 353 | self._dbusservice["/Ac/ActiveIn/L2/V"] = self.grid_items["/Ac/L2/Voltage"].get_value() 354 | else: 355 | self._dbusservice["/Ac/ActiveIn/L2/V"] = grid_nominal_voltage 356 | 357 | # current 358 | if self.ac_load_items["/Ac/L2/Current"] is not None: 359 | self._dbusservice["/Ac/ActiveIn/L2/I"] = self.ac_load_items["/Ac/L2/Current"].get_value() 360 | else: 361 | self._dbusservice["/Ac/ActiveIn/L2/I"] = round(self._dbusservice["/Ac/ActiveIn/L2/P"] / self._dbusservice["/Ac/ActiveIn/L2/V"], 2) 362 | 363 | # L3 ---- 364 | if "L3" in phase_used and self.ac_load_items["/Ac/L3/Power"] is not None: 365 | # power 366 | self._dbusservice["/Ac/ActiveIn/L3/P"] = self.ac_load_items["/Ac/L3/Power"].get_value() 367 | self._dbusservice["/Ac/ActiveIn/L3/S"] = self._dbusservice["/Ac/ActiveIn/L3/P"] 368 | 369 | # frequency 370 | if self.ac_load_items["/Ac/L3/Frequency"] is not None: 371 | self._dbusservice["/Ac/ActiveIn/L3/F"] = self.ac_load_items["/Ac/L3/Frequency"].get_value() 372 | elif self.grid_items != {} and self.grid_items["/Ac/L3/Frequency"] is not None: 373 | self._dbusservice["/Ac/ActiveIn/L3/F"] = self.ac_load_items["/Ac/L3/Frequency"].get_value() 374 | else: 375 | self._dbusservice["/Ac/ActiveIn/L3/F"] = grid_frequency 376 | 377 | # voltage 378 | if self.ac_load_items["/Ac/L3/Voltage"] is not None: 379 | self._dbusservice["/Ac/ActiveIn/L3/V"] = self.ac_load_items["/Ac/L3/Voltage"].get_value() 380 | elif self.grid_items != {} and self.grid_items["/Ac/L3/Voltage"] is not None: 381 | self._dbusservice["/Ac/ActiveIn/L3/V"] = self.grid_items["/Ac/L3/Voltage"].get_value() 382 | else: 383 | self._dbusservice["/Ac/ActiveIn/L3/V"] = grid_nominal_voltage 384 | 385 | # current 386 | if self.ac_load_items["/Ac/L3/Current"] is not None: 387 | self._dbusservice["/Ac/ActiveIn/L3/I"] = self.ac_load_items["/Ac/L3/Current"].get_value() 388 | else: 389 | self._dbusservice["/Ac/ActiveIn/L3/I"] = round(self._dbusservice["/Ac/ActiveIn/L3/P"] / self._dbusservice["/Ac/ActiveIn/L3/V"], 2) 390 | 391 | else: 392 | # calculate ratio of power between each phases 393 | active_in_L1_power = self.system_items["/Ac/ActiveIn/L1/Power"].get_value() if self.system_items["/Ac/ActiveIn/L1/Power"] is not None else 0 394 | active_in_L2_power = self.system_items["/Ac/ActiveIn/L2/Power"].get_value() if self.system_items["/Ac/ActiveIn/L2/Power"] is not None else 0 395 | active_in_L3_power = self.system_items["/Ac/ActiveIn/L3/Power"].get_value() if self.system_items["/Ac/ActiveIn/L3/Power"] is not None else 0 396 | 397 | pv_on_grid_L1_power = self.system_items["/Ac/PvOnGrid/L1/Power"].get_value() if self.system_items["/Ac/PvOnGrid/L1/Power"] is not None else 0 398 | pv_on_grid_L2_power = self.system_items["/Ac/PvOnGrid/L2/Power"].get_value() if self.system_items["/Ac/PvOnGrid/L2/Power"] is not None else 0 399 | pv_on_grid_L3_power = self.system_items["/Ac/PvOnGrid/L3/Power"].get_value() if self.system_items["/Ac/PvOnGrid/L3/Power"] is not None else 0 400 | 401 | ac_total_L1_power = self.zeroIfNone(active_in_L1_power) + self.zeroIfNone(pv_on_grid_L1_power) 402 | ac_total_L2_power = self.zeroIfNone(active_in_L2_power) + self.zeroIfNone(pv_on_grid_L2_power) 403 | ac_total_L3_power = self.zeroIfNone(active_in_L3_power) + self.zeroIfNone(pv_on_grid_L3_power) 404 | ac_total_power = ac_total_L1_power + ac_total_L2_power + ac_total_L3_power 405 | 406 | # calculate the ratio of power between each phases 407 | ratio_L1 = round((ac_total_L1_power / ac_total_power) if ac_total_power != 0 else 0, 4) 408 | ratio_L2 = round((ac_total_L2_power / ac_total_power) if ac_total_power != 0 else 0, 4) 409 | ratio_L3 = round((ac_total_L3_power / ac_total_power) if ac_total_power != 0 else 0, 4) 410 | 411 | logging.debug(f"ratio_L1: {ratio_L1}, ratio_L2: {ratio_L2}, ratio_L3: {ratio_L3}") 412 | 413 | # L1 ----- 414 | if "L1" in phase_used: 415 | # since the MultiPlus emulator is only integrating the power flowing from AC to DC and vice versa, the power is divided by the number of phases 416 | self._dbusservice["/Ac/ActiveIn/L1/P"] = round((dc_power * ratio_L1 if dc_power != 0 else 0), 0) 417 | self._dbusservice["/Ac/ActiveIn/L1/S"] = self._dbusservice["/Ac/ActiveIn/L1/P"] 418 | 419 | if self.grid_items != {} and self.grid_items["/Ac/L1/Frequency"] is not None: 420 | self._dbusservice["/Ac/ActiveIn/L1/F"] = self.grid_items["/Ac/L1/Frequency"].get_value() 421 | else: 422 | self._dbusservice["/Ac/ActiveIn/L1/F"] = grid_frequency 423 | 424 | # voltage 425 | if self.grid_items != {} and self.grid_items["/Ac/L1/Voltage"] is not None: 426 | self._dbusservice["/Ac/ActiveIn/L1/V"] = self.grid_items["/Ac/L1/Voltage"].get_value() 427 | else: 428 | self._dbusservice["/Ac/ActiveIn/L1/V"] = grid_nominal_voltage 429 | 430 | # current 431 | if self.grid_items != {} and self.grid_items["/Ac/L1/Current"] is not None: 432 | self._dbusservice["/Ac/ActiveIn/L1/I"] = self.grid_items["/Ac/L1/Current"].get_value() 433 | else: 434 | self._dbusservice["/Ac/ActiveIn/L1/I"] = round(self._dbusservice["/Ac/ActiveIn/L1/P"] / self._dbusservice["/Ac/ActiveIn/L1/V"], 2) 435 | 436 | # L2 ----- 437 | if "L2" in phase_used: 438 | # since the MultiPlus emulator is only integrating the power flowing from AC to DC and vice versa, the power is divided by the number of phases 439 | self._dbusservice["/Ac/ActiveIn/L2/P"] = round((dc_power * ratio_L2 if dc_power != 0 else 0), 0) 440 | self._dbusservice["/Ac/ActiveIn/L2/S"] = self._dbusservice["/Ac/ActiveIn/L2/P"] 441 | 442 | if self.grid_items != {} and self.grid_items["/Ac/L2/Frequency"] is not None: 443 | self._dbusservice["/Ac/ActiveIn/L2/F"] = self.grid_items["/Ac/L2/Frequency"].get_value() 444 | else: 445 | self._dbusservice["/Ac/ActiveIn/L2/F"] = grid_frequency 446 | 447 | # voltage 448 | if self.grid_items != {} and self.grid_items["/Ac/L2/Voltage"] is not None: 449 | self._dbusservice["/Ac/ActiveIn/L2/V"] = self.grid_items["/Ac/L2/Voltage"].get_value() 450 | else: 451 | self._dbusservice["/Ac/ActiveIn/L2/V"] = grid_nominal_voltage 452 | 453 | # current 454 | if self.grid_items != {} and self.grid_items["/Ac/L2/Current"] is not None: 455 | self._dbusservice["/Ac/ActiveIn/L2/I"] = self.grid_items["/Ac/L2/Current"].get_value() 456 | else: 457 | self._dbusservice["/Ac/ActiveIn/L2/I"] = round(self._dbusservice["/Ac/ActiveIn/L2/P"] / self._dbusservice["/Ac/ActiveIn/L2/V"], 2) 458 | 459 | # L3 ----- 460 | if "L3" in phase_used: 461 | # since the MultiPlus emulator is only integrating the power flowing from AC to DC and vice versa, the power is divided by the number of phases 462 | self._dbusservice["/Ac/ActiveIn/L3/P"] = round((dc_power * ratio_L3 if dc_power != 0 else 0), 0) 463 | self._dbusservice["/Ac/ActiveIn/L3/S"] = self._dbusservice["/Ac/ActiveIn/L3/P"] 464 | 465 | if self.grid_items != {} and self.grid_items["/Ac/L3/Frequency"] is not None: 466 | self._dbusservice["/Ac/ActiveIn/L3/F"] = self.grid_items["/Ac/L3/Frequency"].get_value() 467 | else: 468 | self._dbusservice["/Ac/ActiveIn/L3/F"] = grid_frequency 469 | 470 | # voltage 471 | if self.grid_items != {} and self.grid_items["/Ac/L3/Voltage"] is not None: 472 | self._dbusservice["/Ac/ActiveIn/L3/V"] = self.grid_items["/Ac/L3/Voltage"].get_value() 473 | else: 474 | self._dbusservice["/Ac/ActiveIn/L3/V"] = grid_nominal_voltage 475 | 476 | # current 477 | if self.grid_items != {} and self.grid_items["/Ac/L3/Current"] is not None: 478 | self._dbusservice["/Ac/ActiveIn/L3/I"] = self.grid_items["/Ac/L3/Current"].get_value() 479 | else: 480 | self._dbusservice["/Ac/ActiveIn/L3/I"] = round(self._dbusservice["/Ac/ActiveIn/L3/P"] / self._dbusservice["/Ac/ActiveIn/L3/V"], 2) 481 | 482 | # calculate total values 483 | self._dbusservice["/Ac/ActiveIn/P"] = ( 484 | self.zeroIfNone(self._dbusservice["/Ac/ActiveIn/L1/P"]) + self.zeroIfNone(self._dbusservice["/Ac/ActiveIn/L2/P"]) + self.zeroIfNone(self._dbusservice["/Ac/ActiveIn/L3/P"]) 485 | ) 486 | self._dbusservice["/Ac/ActiveIn/S"] = self._dbusservice["/Ac/ActiveIn/P"] 487 | 488 | # get values from BMS 489 | # for bubble flow in chart and load visualization 490 | self._dbusservice["/Ac/NumberOfPhases"] = phase_count 491 | 492 | # get values from BMS 493 | # for bubble flow in GUI 494 | self._dbusservice["/Dc/0/Current"] = dc_current 495 | # self._dbusservice["/Dc/0/MaxChargeCurrent"] = self.system_items["/Info/MaxChargeCurrent"] 496 | self._dbusservice["/Dc/0/Power"] = dc_power 497 | self._dbusservice["/Dc/0/Temperature"] = self.system_items["/Dc/Battery/Temperature"].get_value() 498 | self._dbusservice["/Dc/0/Voltage"] = dc_voltage 499 | 500 | self._dbusservice["/Devices/0/UpTime"] = int(time()) - time_driver_started 501 | 502 | if phase_count >= 2: 503 | self._dbusservice["/Devices/1/UpTime"] = int(time()) - time_driver_started 504 | 505 | if phase_count == 3: 506 | self._dbusservice["/Devices/2/UpTime"] = int(time()) - time_driver_started 507 | 508 | self._dbusservice["/Energy/InverterToAcOut"] = json_data["dc"]["discharging"] if "dc" in json_data and "discharging" in json_data["dc"] else 0 509 | self._dbusservice["/Energy/OutToInverter"] = json_data["dc"]["charging"] if "dc" in json_data and "charging" in json_data["dc"] else 0 510 | 511 | # self._dbusservice["/Hub/ChargeVoltage"] = self.system_items["/Info/MaxChargeVoltage"] 512 | 513 | # self._dbusservice["/Leds/Absorption"] = 1 if self.system_items["/Info/ChargeMode"].startswith("Absorption") else 0 514 | # self._dbusservice["/Leds/Bulk"] = 1 if self.system_items["/Info/ChargeMode"].startswith("Bulk") else 0 515 | # self._dbusservice["/Leds/Float"] = 1 if self.system_items["/Info/ChargeMode"].startswith("Float") else 0 516 | self._dbusservice["/Soc"] = self.system_items["/Dc/Battery/Soc"].get_value() 517 | 518 | # increment UpdateIndex - to show that new data is available 519 | index = self._dbusservice["/UpdateIndex"] + 1 # increment index 520 | if index > 255: # maximum value of the index 521 | index = 0 # overflow from 255 to 0 522 | self._dbusservice["/UpdateIndex"] = index 523 | 524 | return True 525 | 526 | def _handlechangedvalue(self, path, value): 527 | logging.debug("someone else updated %s to %s" % (path, value)) 528 | return True # accept the change 529 | 530 | 531 | def create_device_dbus_paths(device_number: int = 0): 532 | """ 533 | Create the dbus paths for the device. 534 | """ 535 | 536 | global _w, _n, _v 537 | 538 | paths_dbus = { 539 | f"/Devices/{device_number}/Ac/In/L2/P": {"initial": None, "textformat": _w}, 540 | f"/Devices/{device_number}/Ac/In/P": {"initial": None, "textformat": _w}, 541 | f"/Devices/{device_number}/Ac/Inverter/P": {"initial": None, "textformat": _w}, 542 | f"/Devices/{device_number}/Ac/Out/L2/P": {"initial": None, "textformat": _w}, 543 | f"/Devices/{device_number}/Ac/Out/P": {"initial": None, "textformat": _w}, 544 | f"/Devices/{device_number}/Assistants": { 545 | "initial": [ 546 | 139, 547 | 1, 548 | 0, 549 | 0, 550 | 0, 551 | 0, 552 | 0, 553 | 0, 554 | 0, 555 | 0, 556 | 0, 557 | 0, 558 | 0, 559 | 0, 560 | 0, 561 | 0, 562 | 0, 563 | 0, 564 | 0, 565 | 0, 566 | 0, 567 | 0, 568 | 0, 569 | 0, 570 | 0, 571 | 0, 572 | 0, 573 | 0, 574 | 0, 575 | 0, 576 | 0, 577 | 0, 578 | 0, 579 | 0, 580 | 0, 581 | 0, 582 | 0, 583 | 0, 584 | 0, 585 | 0, 586 | 0, 587 | 0, 588 | 0, 589 | 0, 590 | 0, 591 | 0, 592 | 0, 593 | 0, 594 | 0, 595 | 0, 596 | 0, 597 | 0, 598 | 0, 599 | 0, 600 | 0, 601 | 0, 602 | ], 603 | "textformat": None, 604 | }, 605 | f"/Devices/{device_number}/CNBFirmwareVersion": { 606 | "initial": 2204156, 607 | "textformat": _n, 608 | }, 609 | f"/Devices/{device_number}/Diagnostics/UBatRipple": { 610 | "initial": None, 611 | "textformat": _n, 612 | }, 613 | f"/Devices/{device_number}/Diagnostics/UBatTerminal": { 614 | "initial": None, 615 | "textformat": _v, 616 | }, 617 | f"/Devices/{device_number}/Diagnostics/UBatVSense": { 618 | "initial": None, 619 | "textformat": _v, 620 | }, 621 | f"/Devices/{device_number}/ErrorAndWarningFlags/NSErrConnectFrustratedByRelayTest": { 622 | "initial": 0, 623 | "textformat": _n, 624 | }, 625 | f"/Devices/{device_number}/ErrorAndWarningFlags/NSErrRelayTestKeepsFailing": { 626 | "initial": 0, 627 | "textformat": _n, 628 | }, 629 | f"/Devices/{device_number}/ErrorAndWarningFlags/RawFlags": { 630 | "initial": 0, 631 | "textformat": _n, 632 | }, 633 | f"/Devices/{device_number}/ErrorAndWarningFlags/WarnRelayTestRecentlyFailed": { 634 | "initial": 0, 635 | "textformat": _n, 636 | }, 637 | f"/Devices/{device_number}/ExtendStatus/AcIn1Available": { 638 | "initial": 1, 639 | "textformat": _n, 640 | }, 641 | f"/Devices/{device_number}/ExtendStatus/BolTimeoutOccured": { 642 | "initial": 0, 643 | "textformat": _n, 644 | }, 645 | f"/Devices/{device_number}/ExtendStatus/ChargeDisabledDueToLowTemp": { 646 | "initial": 0, 647 | "textformat": _n, 648 | }, 649 | f"/Devices/{device_number}/ExtendStatus/ChargeIsDisabled": { 650 | "initial": 0, 651 | "textformat": _n, 652 | }, 653 | f"/Devices/{device_number}/ExtendStatus/DMCGeneratorSelected": { 654 | "initial": 0, 655 | "textformat": _n, 656 | }, 657 | f"/Devices/{device_number}/ExtendStatus/GridRelayReport/Code": { 658 | "initial": None, 659 | "textformat": _n, 660 | }, 661 | f"/Devices/{device_number}/ExtendStatus/GridRelayReport/Count": { 662 | "initial": None, 663 | "textformat": _n, 664 | }, 665 | f"/Devices/{device_number}/ExtendStatus/GridRelayReport/Reset": { 666 | "initial": 0, 667 | "textformat": _n, 668 | }, 669 | f"/Devices/{device_number}/ExtendStatus/HighDcCurrent": { 670 | "initial": 0, 671 | "textformat": _n, 672 | }, 673 | f"/Devices/{device_number}/ExtendStatus/HighDcVoltage": { 674 | "initial": 0, 675 | "textformat": _n, 676 | }, 677 | f"/Devices/{device_number}/ExtendStatus/IgnoreAcIn1": { 678 | "initial": 0, 679 | "textformat": _n, 680 | }, 681 | f"/Devices/{device_number}/ExtendStatus/IgnoreAcIn1AssistantsVs": { 682 | "initial": 0, 683 | "textformat": _n, 684 | }, 685 | f"/Devices/{device_number}/ExtendStatus/MainsPllLocked": { 686 | "initial": 1, 687 | "textformat": _n, 688 | }, 689 | f"/Devices/{device_number}/ExtendStatus/NPFGeneratorSelected": { 690 | "initial": 0, 691 | "textformat": _n, 692 | }, 693 | f"/Devices/{device_number}/ExtendStatus/PcvPotmeterOnZero": { 694 | "initial": 0, 695 | "textformat": _n, 696 | }, 697 | f"/Devices/{device_number}/ExtendStatus/PowerPackPreOverload": { 698 | "initial": 0, 699 | "textformat": _n, 700 | }, 701 | f"/Devices/{device_number}/ExtendStatus/PreferRenewableEnergy": { 702 | "initial": 0, 703 | "textformat": _n, 704 | }, 705 | f"/Devices/{device_number}/ExtendStatus/PreferRenewableEnergyActive": { 706 | "initial": 0, 707 | "textformat": _n, 708 | }, 709 | f"/Devices/{device_number}/ExtendStatus/RawFlags0": { 710 | "initial": 268697648, 711 | "textformat": _n, 712 | }, 713 | f"/Devices/{device_number}/ExtendStatus/RawFlags1": { 714 | "initial": None, 715 | "textformat": _n, 716 | }, 717 | f"/Devices/{device_number}/ExtendStatus/RelayTestOk": { 718 | "initial": 1, 719 | "textformat": _n, 720 | }, 721 | f"/Devices/{device_number}/ExtendStatus/SocTooLowToInvert": { 722 | "initial": 0, 723 | "textformat": _n, 724 | }, 725 | f"/Devices/{device_number}/ExtendStatus/SustainMode": { 726 | "initial": 0, 727 | "textformat": _n, 728 | }, 729 | f"/Devices/{device_number}/ExtendStatus/SwitchoverInfo/Connecting": { 730 | "initial": 0, 731 | "textformat": _n, 732 | }, 733 | f"/Devices/{device_number}/ExtendStatus/SwitchoverInfo/Delay": { 734 | "initial": 0, 735 | "textformat": _n, 736 | }, 737 | f"/Devices/{device_number}/ExtendStatus/SwitchoverInfo/ErrorFlags": { 738 | "initial": 0, 739 | "textformat": _n, 740 | }, 741 | f"/Devices/{device_number}/ExtendStatus/TemperatureHighForceBypass": { 742 | "initial": 0, 743 | "textformat": _n, 744 | }, 745 | f"/Devices/{device_number}/ExtendStatus/VeBusNetworkQualityCounter": { 746 | "initial": 0, 747 | "textformat": _n, 748 | }, 749 | f"/Devices/{device_number}/ExtendStatus/WaitingForRelayTest": { 750 | "initial": 0, 751 | "textformat": _n, 752 | }, 753 | f"/Devices/{device_number}/FirmwareSubVersion": { 754 | "initial": 0, 755 | "textformat": _n, 756 | }, 757 | f"/Devices/{device_number}/FirmwareVersion": { 758 | "initial": 1296, 759 | "textformat": _n, 760 | }, 761 | f"/Devices/{device_number}/Info/DeltaTBatNominalTBatMinimum": { 762 | "initial": 45, 763 | "textformat": _n, 764 | }, 765 | f"/Devices/{device_number}/Info/MaximumRelayCurrentAC1": { 766 | "initial": 50, 767 | "textformat": _n, 768 | }, 769 | f"/Devices/{device_number}/Info/MaximumRelayCurrentAC2": { 770 | "initial": 0, 771 | "textformat": _n, 772 | }, 773 | f"/Devices/{device_number}/InterfaceProtectionLog/0/ErrorFlags": { 774 | "initial": None, 775 | "textformat": _n, 776 | }, 777 | f"/Devices/{device_number}/InterfaceProtectionLog/0/Time": { 778 | "initial": None, 779 | "textformat": _n, 780 | }, 781 | f"/Devices/{device_number}/InterfaceProtectionLog/1/ErrorFlags": { 782 | "initial": None, 783 | "textformat": _n, 784 | }, 785 | f"/Devices/{device_number}/InterfaceProtectionLog/1/Time": { 786 | "initial": None, 787 | "textformat": _n, 788 | }, 789 | f"/Devices/{device_number}/InterfaceProtectionLog/2/ErrorFlags": { 790 | "initial": None, 791 | "textformat": _n, 792 | }, 793 | f"/Devices/{device_number}/InterfaceProtectionLog/2/Time": { 794 | "initial": None, 795 | "textformat": _n, 796 | }, 797 | f"/Devices/{device_number}/InterfaceProtectionLog/3/ErrorFlags": { 798 | "initial": None, 799 | "textformat": _n, 800 | }, 801 | f"/Devices/{device_number}/InterfaceProtectionLog/3/Time": { 802 | "initial": None, 803 | "textformat": _n, 804 | }, 805 | f"/Devices/{device_number}/InterfaceProtectionLog/4/ErrorFlags": { 806 | "initial": None, 807 | "textformat": _n, 808 | }, 809 | f"/Devices/{device_number}/InterfaceProtectionLog/4/Time": { 810 | "initial": None, 811 | "textformat": _n, 812 | }, 813 | # ---- 814 | f"/Devices/{device_number}/ProductId": { 815 | "initial": 9763, 816 | "textformat": _n, 817 | }, 818 | f"/Devices/{device_number}/SerialNumber": { 819 | "initial": "HQ00000AA0" + str(device_number + 1), 820 | "textformat": _s, 821 | }, 822 | f"/Devices/{device_number}/Settings/AssistCurrentBoostFactor": { 823 | "initial": 2.0, 824 | "textformat": _n1, 825 | }, 826 | f"/Devices/{device_number}/Settings/InverterOutputVoltage": { 827 | "initial": 230.0, 828 | "textformat": _n1, 829 | }, 830 | f"/Devices/{device_number}/Settings/PowerAssistEnabled": { 831 | "initial": False, 832 | "textformat": None, 833 | }, 834 | f"/Devices/{device_number}/Settings/ReadProgress": { 835 | "initial": 100, 836 | "textformat": _n, 837 | }, 838 | f"/Devices/{device_number}/Settings/ResetRequired": { 839 | "initial": 0, 840 | "textformat": _n, 841 | }, 842 | f"/Devices/{device_number}/Settings/UpsFunction": { 843 | "initial": False, 844 | "textformat": None, 845 | }, 846 | f"/Devices/{device_number}/Settings/WriteProgress": { 847 | "initial": None, 848 | "textformat": _n, 849 | }, 850 | f"/Devices/{device_number}/UpTime": { 851 | "initial": 0, 852 | "textformat": _n, 853 | }, 854 | f"/Devices/{device_number}/Version": {"initial": 2987520, "textformat": _s}, 855 | } 856 | 857 | return paths_dbus 858 | 859 | 860 | def setup_dbus_external_items(): 861 | global dbus_service_name_grid, dbus_service_name_ac_load 862 | 863 | # setup external dbus paths 864 | # connect to the sessionbus, on a CC GX the systembus is used 865 | dbus_connection = dbus.SessionBus() if "DBUS_SESSION_BUS_ADDRESS" in os.environ else dbus.SystemBus() 866 | 867 | # list of dbus services 868 | dbus_services = dbus_connection.list_names() 869 | 870 | # ----- BATTERY ----- 871 | # check if the dbus service is available 872 | dbus_service_system = "com.victronenergy.system" 873 | is_present_in_vebus = dbus_service_system in dbus_services 874 | 875 | # dictionary containing the different items 876 | dbus_objects_system = {} 877 | 878 | if is_present_in_vebus: 879 | dbus_objects_system["/Ac/ActiveIn/L1/Power"] = VeDbusItemImport(dbus_connection, dbus_service_system, "/Ac/ActiveIn/L1/Power") 880 | dbus_objects_system["/Ac/ActiveIn/L2/Power"] = VeDbusItemImport(dbus_connection, dbus_service_system, "/Ac/ActiveIn/L2/Power") 881 | dbus_objects_system["/Ac/ActiveIn/L3/Power"] = VeDbusItemImport(dbus_connection, dbus_service_system, "/Ac/ActiveIn/L3/Power") 882 | 883 | dbus_objects_system["/Ac/PvOnGrid/L1/Power"] = VeDbusItemImport(dbus_connection, dbus_service_system, "/Ac/PvOnGrid/L1/Power") 884 | dbus_objects_system["/Ac/PvOnGrid/L2/Power"] = VeDbusItemImport(dbus_connection, dbus_service_system, "/Ac/PvOnGrid/L2/Power") 885 | dbus_objects_system["/Ac/PvOnGrid/L3/Power"] = VeDbusItemImport(dbus_connection, dbus_service_system, "/Ac/PvOnGrid/L3/Power") 886 | 887 | dbus_objects_system["/Dc/Battery/BatteryService"] = VeDbusItemImport(dbus_connection, dbus_service_system, "/Dc/Battery/BatteryService") 888 | dbus_objects_system["/Dc/Battery/Current"] = VeDbusItemImport(dbus_connection, dbus_service_system, "/Dc/Battery/Current") 889 | dbus_objects_system["/Dc/Battery/Power"] = VeDbusItemImport(dbus_connection, dbus_service_system, "/Dc/Battery/Power") 890 | dbus_objects_system["/Dc/Battery/Temperature"] = VeDbusItemImport(dbus_connection, dbus_service_system, "/Dc/Battery/Temperature") 891 | dbus_objects_system["/Dc/Battery/Voltage"] = VeDbusItemImport(dbus_connection, dbus_service_system, "/Dc/Battery/Voltage") 892 | dbus_objects_system["/Dc/Battery/Soc"] = VeDbusItemImport(dbus_connection, dbus_service_system, "/Dc/Battery/Soc") 893 | 894 | # ----- GRID ----- 895 | is_present_in_vebus = False 896 | 897 | # check if the dbus service is available 898 | if dbus_service_name_grid != "": 899 | logging.info(f"Fetched dbus_service_name_grid from config: {dbus_service_name_grid}") 900 | is_present_in_vebus = dbus_service_name_grid in dbus_services 901 | # search for the first com.victronenergy.grid service 902 | else: 903 | # create variable to store the first grid service name 904 | dbus_service_name_grid = None 905 | 906 | # iterate through the array to find the first string containing "com.victronenergy.grid" 907 | for name in dbus_services: 908 | if "com.victronenergy.grid" in name: 909 | dbus_service_name_grid = name 910 | is_present_in_vebus = True 911 | break 912 | 913 | if dbus_service_name_grid is not None: 914 | logging.info(f"No grid service name provided, using the first one found: {dbus_service_name_grid}") 915 | 916 | # dictionary containing the different items 917 | dbus_objects_grid = {} 918 | 919 | if is_present_in_vebus: 920 | logging.info(f"{dbus_service_name_grid} is present in dbus, setting up the grid values") 921 | dbus_objects_grid["/Ac/L1/Power"] = VeDbusItemImport(dbus_connection, dbus_service_name_grid, "/Ac/L1/Power") 922 | dbus_objects_grid["/Ac/L1/Current"] = VeDbusItemImport(dbus_connection, dbus_service_name_grid, "/Ac/L1/Current") 923 | dbus_objects_grid["/Ac/L1/Voltage"] = VeDbusItemImport(dbus_connection, dbus_service_name_grid, "/Ac/L1/Voltage") 924 | dbus_objects_grid["/Ac/L1/Frequency"] = VeDbusItemImport(dbus_connection, dbus_service_name_grid, "/Ac/L1/Frequency") 925 | 926 | dbus_objects_grid["/Ac/L2/Power"] = VeDbusItemImport(dbus_connection, dbus_service_name_grid, "/Ac/L2/Power") 927 | dbus_objects_grid["/Ac/L2/Current"] = VeDbusItemImport(dbus_connection, dbus_service_name_grid, "/Ac/L2/Current") 928 | dbus_objects_grid["/Ac/L2/Voltage"] = VeDbusItemImport(dbus_connection, dbus_service_name_grid, "/Ac/L2/Voltage") 929 | dbus_objects_grid["/Ac/L2/Frequency"] = VeDbusItemImport(dbus_connection, dbus_service_name_grid, "/Ac/L2/Frequency") 930 | 931 | dbus_objects_grid["/Ac/L3/Power"] = VeDbusItemImport(dbus_connection, dbus_service_name_grid, "/Ac/L3/Power") 932 | dbus_objects_grid["/Ac/L3/Current"] = VeDbusItemImport(dbus_connection, dbus_service_name_grid, "/Ac/L3/Current") 933 | dbus_objects_grid["/Ac/L3/Voltage"] = VeDbusItemImport(dbus_connection, dbus_service_name_grid, "/Ac/L3/Voltage") 934 | dbus_objects_grid["/Ac/L3/Frequency"] = VeDbusItemImport(dbus_connection, dbus_service_name_grid, "/Ac/L3/Frequency") 935 | 936 | dbus_objects_grid["/Ac/Power"] = VeDbusItemImport(dbus_connection, dbus_service_name_grid, "/Ac/Power") 937 | dbus_objects_grid["/Ac/Current"] = VeDbusItemImport(dbus_connection, dbus_service_name_grid, "/Ac/Current") 938 | dbus_objects_grid["/Ac/Voltage"] = VeDbusItemImport(dbus_connection, dbus_service_name_grid, "/Ac/Voltage") 939 | 940 | # ----- AC LOAD ----- 941 | is_present_in_vebus = False 942 | 943 | # check if the dbus service is available 944 | if dbus_service_name_ac_load != "": 945 | logging.info(f"Fetched dbus_service_name_ac_load from config: {dbus_service_name_ac_load}") 946 | is_present_in_vebus = dbus_service_name_ac_load in dbus_services 947 | # search for the first com.victronenergy.acload service 948 | else: 949 | # create variable to store the first ac load service name 950 | dbus_service_name_ac_load = None 951 | 952 | # iterate through the array to find the first string containing "com.victronenergy.acload" 953 | for name in dbus_services: 954 | if "com.victronenergy.acload" in name: 955 | dbus_service_name_ac_load = name 956 | is_present_in_vebus = True 957 | break 958 | 959 | if dbus_service_name_ac_load is not None: 960 | logging.info(f"No AC Load service name provided, using the first one found: {dbus_service_name_ac_load}") 961 | 962 | # dictionary containing the different items 963 | dbus_objects_ac_load = {} 964 | 965 | if is_present_in_vebus: 966 | logging.info(f"{dbus_service_name_ac_load} is present in dbus, setting up the ac load values") 967 | dbus_objects_ac_load["/Ac/L1/Power"] = VeDbusItemImport(dbus_connection, dbus_service_name_ac_load, "/Ac/L1/Power") 968 | dbus_objects_ac_load["/Ac/L1/Current"] = VeDbusItemImport(dbus_connection, dbus_service_name_ac_load, "/Ac/L1/Current") 969 | dbus_objects_ac_load["/Ac/L1/Voltage"] = VeDbusItemImport(dbus_connection, dbus_service_name_ac_load, "/Ac/L1/Voltage") 970 | dbus_objects_ac_load["/Ac/L1/Frequency"] = VeDbusItemImport(dbus_connection, dbus_service_name_ac_load, "/Ac/L1/Frequency") 971 | 972 | dbus_objects_ac_load["/Ac/L2/Power"] = VeDbusItemImport(dbus_connection, dbus_service_name_ac_load, "/Ac/L2/Power") 973 | dbus_objects_ac_load["/Ac/L2/Current"] = VeDbusItemImport(dbus_connection, dbus_service_name_ac_load, "/Ac/L2/Current") 974 | dbus_objects_ac_load["/Ac/L2/Voltage"] = VeDbusItemImport(dbus_connection, dbus_service_name_ac_load, "/Ac/L2/Voltage") 975 | dbus_objects_ac_load["/Ac/L2/Frequency"] = VeDbusItemImport(dbus_connection, dbus_service_name_ac_load, "/Ac/L2/Frequency") 976 | 977 | dbus_objects_ac_load["/Ac/L3/Power"] = VeDbusItemImport(dbus_connection, dbus_service_name_ac_load, "/Ac/L3/Power") 978 | dbus_objects_ac_load["/Ac/L3/Current"] = VeDbusItemImport(dbus_connection, dbus_service_name_ac_load, "/Ac/L3/Current") 979 | dbus_objects_ac_load["/Ac/L3/Voltage"] = VeDbusItemImport(dbus_connection, dbus_service_name_ac_load, "/Ac/L3/Voltage") 980 | dbus_objects_ac_load["/Ac/L3/Frequency"] = VeDbusItemImport(dbus_connection, dbus_service_name_ac_load, "/Ac/L3/Frequency") 981 | 982 | dbus_objects_ac_load["/Ac/Power"] = VeDbusItemImport(dbus_connection, dbus_service_name_ac_load, "/Ac/Power") 983 | dbus_objects_ac_load["/Ac/Current"] = VeDbusItemImport(dbus_connection, dbus_service_name_ac_load, "/Ac/Current") 984 | dbus_objects_ac_load["/Ac/Voltage"] = VeDbusItemImport(dbus_connection, dbus_service_name_ac_load, "/Ac/Voltage") 985 | 986 | logging.info("*** Found values ***") 987 | 988 | if dbus_service_system != "": 989 | logging.info(f"Dbus system service name: {dbus_service_system}") 990 | for item in dbus_objects_system: 991 | # remove items that does not exist 992 | if dbus_objects_system[item].exists: 993 | logging.info(f"{item} = {dbus_objects_system[item].get_value()}") 994 | else: 995 | dbus_objects_system[item] = None 996 | logging.debug(f"{item} does not exist, removed from grid values") 997 | 998 | if dbus_service_name_grid != "": 999 | logging.info(f"Dbus grid service name: {dbus_service_name_grid}") 1000 | for item in dbus_objects_grid: 1001 | # remove items that does not exist 1002 | if dbus_objects_grid[item].exists: 1003 | logging.info(f"{item} = {dbus_objects_grid[item].get_value()}") 1004 | else: 1005 | dbus_objects_grid[item] = None 1006 | logging.debug(f"{item} does not exist, removed from grid values") 1007 | 1008 | if dbus_service_name_ac_load != "": 1009 | logging.info(f"Dbus ac load service name: {dbus_service_name_ac_load}") 1010 | for item in dbus_objects_ac_load: 1011 | # remove items that does not exist 1012 | if dbus_objects_ac_load[item].exists: 1013 | logging.info(f"{item} = {dbus_objects_ac_load[item].get_value()}") 1014 | else: 1015 | dbus_objects_ac_load[item] = None 1016 | logging.debug(f"{item} does not exist, removed from ac_load values") 1017 | 1018 | return dbus_objects_system, dbus_objects_grid, dbus_objects_ac_load 1019 | 1020 | 1021 | # formatting 1022 | def _wh(p, v): 1023 | return str("%.2f" % v) + "Wh" 1024 | 1025 | 1026 | def _a(p, v): 1027 | return str("%.2f" % v) + "A" 1028 | 1029 | 1030 | def _w(p, v): 1031 | return str("%i" % v) + "W" 1032 | 1033 | 1034 | def _va(p, v): 1035 | return str("%i" % v) + "VA" 1036 | 1037 | 1038 | def _v(p, v): 1039 | return str("%i" % v) + "V" 1040 | 1041 | 1042 | def _hz(p, v): 1043 | return str("%.4f" % v) + "Hz" 1044 | 1045 | 1046 | def _c(p, v): 1047 | return str("%i" % v) + "°C" 1048 | 1049 | 1050 | def _percent(p, v): 1051 | return str("%.1f" % v) + "%" 1052 | 1053 | 1054 | def _n(p, v): 1055 | return str("%i" % v) 1056 | 1057 | 1058 | def _n1(p, v): 1059 | return str("%.1f" % v) 1060 | 1061 | 1062 | def _s(p, v): 1063 | return str("%s" % v) 1064 | 1065 | 1066 | def main(): 1067 | global time_driver_started 1068 | 1069 | _thread.daemon = True # allow the program to quit 1070 | 1071 | # Have a mainloop, so we can send/receive asynchronous calls to and from dbus 1072 | DBusGMainLoop(set_as_default=True) 1073 | 1074 | paths_multiplus_dbus = { 1075 | "/Ac/ActiveIn/ActiveInput": {"initial": 0, "textformat": _n}, 1076 | "/Ac/ActiveIn/Connected": {"initial": 1, "textformat": _n}, 1077 | # "/Ac/ActiveIn/CurrentLimit": {"initial": 50.0, "textformat": _a}, # needs also a min and max value 1078 | "/Ac/ActiveIn/CurrentLimitIsAdjustable": {"initial": 1, "textformat": _n}, 1079 | # ---- 1080 | "/Ac/ActiveIn/L1/F": {"initial": None, "textformat": _hz}, 1081 | "/Ac/ActiveIn/L1/I": {"initial": None, "textformat": _a}, 1082 | "/Ac/ActiveIn/L1/P": {"initial": None, "textformat": _w}, 1083 | "/Ac/ActiveIn/L1/S": {"initial": None, "textformat": _va}, 1084 | "/Ac/ActiveIn/L1/V": {"initial": None, "textformat": _v}, 1085 | # ---- 1086 | "/Ac/ActiveIn/L2/F": {"initial": None, "textformat": _hz}, 1087 | "/Ac/ActiveIn/L2/I": {"initial": None, "textformat": _a}, 1088 | "/Ac/ActiveIn/L2/P": {"initial": None, "textformat": _w}, 1089 | "/Ac/ActiveIn/L2/S": {"initial": None, "textformat": _va}, 1090 | "/Ac/ActiveIn/L2/V": {"initial": None, "textformat": _v}, 1091 | # ---- 1092 | "/Ac/ActiveIn/L3/F": {"initial": None, "textformat": _hz}, 1093 | "/Ac/ActiveIn/L3/I": {"initial": None, "textformat": _a}, 1094 | "/Ac/ActiveIn/L3/P": {"initial": None, "textformat": _w}, 1095 | "/Ac/ActiveIn/L3/S": {"initial": None, "textformat": _va}, 1096 | "/Ac/ActiveIn/L3/V": {"initial": None, "textformat": _v}, 1097 | # ---- 1098 | "/Ac/ActiveIn/P": {"initial": 0, "textformat": _w}, 1099 | "/Ac/ActiveIn/S": {"initial": 0, "textformat": _va}, 1100 | # ---- 1101 | "/Ac/Control/IgnoreAcIn1": {"initial": 0, "textformat": _n}, 1102 | "/Ac/Control/RemoteGeneratorSelected": {"initial": 0, "textformat": _n}, 1103 | # ---- 1104 | "/Ac/In/1/CurrentLimit": {"initial": 50.0, "textformat": _a}, 1105 | "/Ac/In/1/CurrentLimitIsAdjustable": {"initial": 1, "textformat": _n}, 1106 | # ---- 1107 | "/Ac/In/2/CurrentLimit": {"initial": None, "textformat": _a}, 1108 | "/Ac/In/2/CurrentLimitIsAdjustable": {"initial": None, "textformat": _n}, 1109 | # ---- 1110 | "/Ac/NumberOfAcInputs": {"initial": 1, "textformat": _n}, 1111 | "/Ac/NumberOfPhases": {"initial": phase_count, "textformat": _n}, 1112 | # ---- 1113 | "/Ac/Out/L1/F": {"initial": None, "textformat": _hz}, 1114 | "/Ac/Out/L1/I": {"initial": None, "textformat": _a}, 1115 | "/Ac/Out/L1/NominalInverterPower": {"initial": None, "textformat": _w}, 1116 | "/Ac/Out/L1/P": {"initial": None, "textformat": _w}, 1117 | "/Ac/Out/L1/S": {"initial": None, "textformat": _va}, 1118 | "/Ac/Out/L1/V": {"initial": None, "textformat": _v}, 1119 | # ---- 1120 | "/Ac/Out/L2/F": {"initial": None, "textformat": _hz}, 1121 | "/Ac/Out/L2/I": {"initial": None, "textformat": _a}, 1122 | "/Ac/Out/L2/NominalInverterPower": {"initial": None, "textformat": _w}, 1123 | "/Ac/Out/L2/P": {"initial": None, "textformat": _w}, 1124 | "/Ac/Out/L2/S": {"initial": None, "textformat": _va}, 1125 | "/Ac/Out/L2/V": {"initial": None, "textformat": _v}, 1126 | # ---- 1127 | "/Ac/Out/L3/F": {"initial": None, "textformat": _hz}, 1128 | "/Ac/Out/L3/I": {"initial": None, "textformat": _a}, 1129 | "/Ac/Out/L3/NominalInverterPower": {"initial": None, "textformat": _w}, 1130 | "/Ac/Out/L3/P": {"initial": None, "textformat": _w}, 1131 | "/Ac/Out/L3/S": {"initial": None, "textformat": _va}, 1132 | "/Ac/Out/L3/V": {"initial": None, "textformat": _v}, 1133 | # ---- 1134 | "/Ac/Out/NominalInverterPower": {"initial": None, "textformat": _w}, 1135 | "/Ac/Out/P": {"initial": None, "textformat": _w}, 1136 | "/Ac/Out/S": {"initial": None, "textformat": _va}, 1137 | # ---- 1138 | "/Ac/PowerMeasurementType": {"initial": 4, "textformat": _n}, 1139 | "/Ac/State/AcIn1Available": {"initial": 1, "textformat": _n}, 1140 | "/Ac/State/IgnoreAcIn1": {"initial": 0, "textformat": _n}, 1141 | "/Ac/State/RemoteGeneratorSelected": {"initial": 0, "textformat": _n}, 1142 | "/Ac/State/SplitPhaseL2L1OutSummed": {"initial": None, "textformat": _n}, 1143 | "/Ac/State/SplitPhaseL2Passthru": {"initial": None, "textformat": _n}, 1144 | # ---- 1145 | "/AcSensor/0/Current": {"initial": None, "textformat": _a}, 1146 | "/AcSensor/0/Energy": {"initial": None, "textformat": _wh}, 1147 | "/AcSensor/0/Location": {"initial": None, "textformat": _s}, 1148 | "/AcSensor/0/Phase": {"initial": None, "textformat": _n}, 1149 | "/AcSensor/0/Power": {"initial": None, "textformat": _w}, 1150 | "/AcSensor/0/Voltage": {"initial": None, "textformat": _v}, 1151 | "/AcSensor/1/Current": {"initial": None, "textformat": _a}, 1152 | "/AcSensor/1/Energy": {"initial": None, "textformat": _wh}, 1153 | "/AcSensor/1/Location": {"initial": None, "textformat": _s}, 1154 | "/AcSensor/1/Phase": {"initial": None, "textformat": _n}, 1155 | "/AcSensor/1/Power": {"initial": None, "textformat": _w}, 1156 | "/AcSensor/1/Voltage": {"initial": None, "textformat": _v}, 1157 | "/AcSensor/2/Current": {"initial": None, "textformat": _a}, 1158 | "/AcSensor/2/Energy": {"initial": None, "textformat": _wh}, 1159 | "/AcSensor/2/Location": {"initial": None, "textformat": _s}, 1160 | "/AcSensor/2/Phase": {"initial": None, "textformat": _n}, 1161 | "/AcSensor/2/Power": {"initial": None, "textformat": _w}, 1162 | "/AcSensor/2/Voltage": {"initial": None, "textformat": _v}, 1163 | "/AcSensor/3/Current": {"initial": None, "textformat": _a}, 1164 | "/AcSensor/3/Energy": {"initial": None, "textformat": _wh}, 1165 | "/AcSensor/3/Location": {"initial": None, "textformat": _s}, 1166 | "/AcSensor/3/Phase": {"initial": None, "textformat": _n}, 1167 | "/AcSensor/3/Power": {"initial": None, "textformat": _w}, 1168 | "/AcSensor/3/Voltage": {"initial": None, "textformat": _v}, 1169 | "/AcSensor/4/Current": {"initial": None, "textformat": _a}, 1170 | "/AcSensor/4/Energy": {"initial": None, "textformat": _wh}, 1171 | "/AcSensor/4/Location": {"initial": None, "textformat": _s}, 1172 | "/AcSensor/4/Phase": {"initial": None, "textformat": _n}, 1173 | "/AcSensor/4/Power": {"initial": None, "textformat": _w}, 1174 | "/AcSensor/4/Voltage": {"initial": None, "textformat": _v}, 1175 | "/AcSensor/5/Current": {"initial": None, "textformat": _a}, 1176 | "/AcSensor/5/Energy": {"initial": None, "textformat": _wh}, 1177 | "/AcSensor/5/Location": {"initial": None, "textformat": _s}, 1178 | "/AcSensor/5/Phase": {"initial": None, "textformat": _n}, 1179 | "/AcSensor/5/Power": {"initial": None, "textformat": _w}, 1180 | "/AcSensor/5/Voltage": {"initial": None, "textformat": _v}, 1181 | "/AcSensor/6/Current": {"initial": None, "textformat": _a}, 1182 | "/AcSensor/6/Energy": {"initial": None, "textformat": _wh}, 1183 | "/AcSensor/6/Location": {"initial": None, "textformat": _s}, 1184 | "/AcSensor/6/Phase": {"initial": None, "textformat": _n}, 1185 | "/AcSensor/6/Power": {"initial": None, "textformat": _w}, 1186 | "/AcSensor/6/Voltage": {"initial": None, "textformat": _v}, 1187 | "/AcSensor/7/Current": {"initial": None, "textformat": _a}, 1188 | "/AcSensor/7/Energy": {"initial": None, "textformat": _wh}, 1189 | "/AcSensor/7/Location": {"initial": None, "textformat": _s}, 1190 | "/AcSensor/7/Phase": {"initial": None, "textformat": _n}, 1191 | "/AcSensor/7/Power": {"initial": None, "textformat": _w}, 1192 | "/AcSensor/7/Voltage": {"initial": None, "textformat": _v}, 1193 | "/AcSensor/8/Current": {"initial": None, "textformat": _a}, 1194 | "/AcSensor/8/Energy": {"initial": None, "textformat": _wh}, 1195 | "/AcSensor/8/Location": {"initial": None, "textformat": _s}, 1196 | "/AcSensor/8/Phase": {"initial": None, "textformat": _n}, 1197 | "/AcSensor/8/Power": {"initial": None, "textformat": _w}, 1198 | "/AcSensor/8/Voltage": {"initial": None, "textformat": _v}, 1199 | "/AcSensor/Count": {"initial": None, "textformat": _n}, 1200 | # ---- 1201 | "/Alarms/BmsConnectionLost": {"initial": 0, "textformat": _n}, 1202 | "/Alarms/BmsPreAlarm": {"initial": None, "textformat": _n}, 1203 | "/Alarms/GridLost": {"initial": 0, "textformat": _n}, 1204 | "/Alarms/HighDcCurrent": {"initial": 0, "textformat": _n}, 1205 | "/Alarms/HighDcVoltage": {"initial": 0, "textformat": _n}, 1206 | "/Alarms/HighTemperature": {"initial": 0, "textformat": _n}, 1207 | "/Alarms/L1/HighTemperature": {"initial": 0, "textformat": _n}, 1208 | "/Alarms/L1/InverterImbalance": {"initial": 0, "textformat": _n}, 1209 | "/Alarms/L1/LowBattery": {"initial": 0, "textformat": _n}, 1210 | "/Alarms/L1/MainsImbalance": {"initial": 0, "textformat": _n}, 1211 | "/Alarms/L1/Overload": {"initial": 0, "textformat": _n}, 1212 | "/Alarms/L1/Ripple": {"initial": 0, "textformat": _n}, 1213 | "/Alarms/L2/HighTemperature": {"initial": 0, "textformat": _n}, 1214 | "/Alarms/L2/InverterImbalance": {"initial": 0, "textformat": _n}, 1215 | "/Alarms/L2/LowBattery": {"initial": 0, "textformat": _n}, 1216 | "/Alarms/L2/MainsImbalance": {"initial": 0, "textformat": _n}, 1217 | "/Alarms/L2/Overload": {"initial": 0, "textformat": _n}, 1218 | "/Alarms/L2/Ripple": {"initial": 0, "textformat": _n}, 1219 | "/Alarms/L3/HighTemperature": {"initial": 0, "textformat": _n}, 1220 | "/Alarms/L3/InverterImbalance": {"initial": 0, "textformat": _n}, 1221 | "/Alarms/L3/LowBattery": {"initial": 0, "textformat": _n}, 1222 | "/Alarms/L3/MainsImbalance": {"initial": 0, "textformat": _n}, 1223 | "/Alarms/L3/Overload": {"initial": 0, "textformat": _n}, 1224 | "/Alarms/L3/Ripple": {"initial": 0, "textformat": _n}, 1225 | "/Alarms/LowBattery": {"initial": 0, "textformat": _n}, 1226 | "/Alarms/Overload": {"initial": 0, "textformat": _n}, 1227 | "/Alarms/PhaseRotation": {"initial": 0, "textformat": _n}, 1228 | "/Alarms/Ripple": {"initial": 0, "textformat": _n}, 1229 | "/Alarms/TemperatureSensor": {"initial": 0, "textformat": _n}, 1230 | "/Alarms/VoltageSensor": {"initial": 0, "textformat": _n}, 1231 | # ---- 1232 | "/BatteryOperationalLimits/BatteryLowVoltage": { 1233 | "initial": None, 1234 | "textformat": _v, 1235 | }, 1236 | "/BatteryOperationalLimits/MaxChargeCurrent": { 1237 | "initial": None, 1238 | "textformat": _a, 1239 | }, 1240 | "/BatteryOperationalLimits/MaxChargeVoltage": { 1241 | "initial": None, 1242 | "textformat": _v, 1243 | }, 1244 | "/BatteryOperationalLimits/MaxDischargeCurrent": { 1245 | "initial": None, 1246 | "textformat": _a, 1247 | }, 1248 | "/BatterySense/Temperature": {"initial": None, "textformat": _c}, 1249 | "/BatterySense/Voltage": {"initial": None, "textformat": _v}, 1250 | # ---- 1251 | "/Bms/AllowToCharge": {"initial": 1, "textformat": _n}, 1252 | "/Bms/AllowToChargeRate": {"initial": 0, "textformat": _n}, 1253 | "/Bms/AllowToDischarge": {"initial": 1, "textformat": _n}, 1254 | "/Bms/BmsExpected": {"initial": 0, "textformat": _n}, 1255 | "/Bms/BmsType": {"initial": 0, "textformat": _n}, 1256 | "/Bms/Error": {"initial": 0, "textformat": _n}, 1257 | "/Bms/PreAlarm": {"initial": None, "textformat": _n}, 1258 | # ---- 1259 | "/Dc/0/Current": {"initial": None, "textformat": _a}, 1260 | "/Dc/0/MaxChargeCurrent": {"initial": None, "textformat": _a}, 1261 | "/Dc/0/Power": {"initial": None, "textformat": _w}, 1262 | "/Dc/0/PreferRenewableEnergy": {"initial": None, "textformat": _n}, 1263 | "/Dc/0/Temperature": {"initial": None, "textformat": _c}, 1264 | "/Dc/0/Voltage": {"initial": None, "textformat": _v}, 1265 | } 1266 | 1267 | # ---- 1268 | # Device 0 1269 | # ---- 1270 | paths_multiplus_dbus.update(create_device_dbus_paths(0)) 1271 | 1272 | if phase_count >= 2: 1273 | # ---- 1274 | # Device 1 1275 | # ---- 1276 | paths_multiplus_dbus.update(create_device_dbus_paths(1)) 1277 | 1278 | if phase_count == 3: 1279 | # ---- 1280 | # Device 2 1281 | # ---- 1282 | paths_multiplus_dbus.update(create_device_dbus_paths(2)) 1283 | 1284 | paths_multiplus_dbus.update( 1285 | { 1286 | # ---- 1287 | "/Devices/Bms/Version": {"initial": None, "textformat": _s}, 1288 | "/Devices/Dmc/Version": {"initial": None, "textformat": _s}, 1289 | "/Devices/NumberOfMultis": {"initial": phase_count, "textformat": _n}, 1290 | # ---- 1291 | "/Energy/AcIn1ToAcOut": {"initial": None, "textformat": _n}, 1292 | "/Energy/AcIn1ToInverter": {"initial": None, "textformat": _n}, 1293 | "/Energy/AcIn2ToAcOut": {"initial": None, "textformat": _n}, 1294 | "/Energy/AcIn2ToInverter": {"initial": None, "textformat": _n}, 1295 | "/Energy/AcOutToAcIn1": {"initial": None, "textformat": _n}, 1296 | "/Energy/AcOutToAcIn2": {"initial": None, "textformat": _n}, 1297 | "/Energy/InverterToAcIn1": {"initial": None, "textformat": _n}, 1298 | "/Energy/InverterToAcIn2": {"initial": None, "textformat": _n}, 1299 | "/Energy/InverterToAcOut": {"initial": None, "textformat": _n}, 1300 | "/Energy/OutToInverter": {"initial": None, "textformat": _n}, 1301 | "/ExtraBatteryCurrent": {"initial": None, "textformat": _n}, 1302 | # ---- 1303 | "/FirmwareFeatures/BolFrame": {"initial": 1, "textformat": _n}, 1304 | "/FirmwareFeatures/BolUBatAndTBatSense": {"initial": 1, "textformat": _n}, 1305 | "/FirmwareFeatures/CommandWriteViaId": {"initial": 1, "textformat": _n}, 1306 | "/FirmwareFeatures/IBatSOCBroadcast": {"initial": 1, "textformat": _n}, 1307 | "/FirmwareFeatures/NewPanelFrame": {"initial": 1, "textformat": _n}, 1308 | "/FirmwareFeatures/SetChargeState": {"initial": 1, "textformat": _n}, 1309 | "/FirmwareSubVersion": {"initial": 0, "textformat": _n}, 1310 | # ---- 1311 | "/Hub/ChargeVoltage": {"initial": None, "textformat": _n}, 1312 | "/Hub4/AssistantId": {"initial": 5, "textformat": _n}, 1313 | "/Hub4/DisableCharge": {"initial": 0, "textformat": _n}, 1314 | "/Hub4/DisableFeedIn": {"initial": 0, "textformat": _n}, 1315 | "/Hub4/DoNotFeedInOvervoltage": {"initial": 1, "textformat": _n}, 1316 | "/Hub4/FixSolarOffsetTo100mV": {"initial": 1, "textformat": _n}, 1317 | } 1318 | ) 1319 | 1320 | paths_multiplus_dbus.update( 1321 | { 1322 | # com.victronenergy.settings/Settings/CGwacs/AcPowerSetPoint 1323 | # if positive then same value, if negative value +1 1324 | "/Hub4/L1/AcPowerSetpoint": {"initial": 0, "textformat": _n}, 1325 | "/Hub4/L1/CurrentLimitedDueToHighTemp": {"initial": 0, "textformat": _n}, 1326 | "/Hub4/L1/FrequencyVariationOccurred": {"initial": 0, "textformat": _n}, 1327 | "/Hub4/L1/MaxFeedInPower": {"initial": 32766, "textformat": _n}, 1328 | "/Hub4/L1/OffsetAddedToVoltageSetpoint": {"initial": 0, "textformat": _n}, 1329 | "/Hub4/L1/OverruledShoreLimit": {"initial": None, "textformat": _n}, 1330 | } 1331 | ) 1332 | 1333 | if phase_count >= 2: 1334 | paths_multiplus_dbus.update( 1335 | { 1336 | # com.victronenergy.settings/Settings/CGwacs/AcPowerSetPoint 1337 | # if positive then same value, if negative value +1 1338 | "/Hub4/L2/AcPowerSetpoint": {"initial": 0, "textformat": _n}, 1339 | "/Hub4/L2/CurrentLimitedDueToHighTemp": { 1340 | "initial": 0, 1341 | "textformat": _n, 1342 | }, 1343 | "/Hub4/L2/FrequencyVariationOccurred": {"initial": 0, "textformat": _n}, 1344 | "/Hub4/L2/MaxFeedInPower": {"initial": 32766, "textformat": _n}, 1345 | "/Hub4/L2/OffsetAddedToVoltageSetpoint": { 1346 | "initial": 0, 1347 | "textformat": _n, 1348 | }, 1349 | "/Hub4/L2/OverruledShoreLimit": {"initial": None, "textformat": _n}, 1350 | } 1351 | ) 1352 | 1353 | if phase_count == 3: 1354 | paths_multiplus_dbus.update( 1355 | { 1356 | # com.victronenergy.settings/Settings/CGwacs/AcPowerSetPoint 1357 | # if positive then same value, if negative value +1 1358 | "/Hub4/L3/AcPowerSetpoint": {"initial": 0, "textformat": _n}, 1359 | "/Hub4/L3/CurrentLimitedDueToHighTemp": { 1360 | "initial": 0, 1361 | "textformat": _n, 1362 | }, 1363 | "/Hub4/L3/FrequencyVariationOccurred": {"initial": 0, "textformat": _n}, 1364 | "/Hub4/L3/MaxFeedInPower": {"initial": 32766, "textformat": _n}, 1365 | "/Hub4/L3/OffsetAddedToVoltageSetpoint": { 1366 | "initial": 0, 1367 | "textformat": _n, 1368 | }, 1369 | "/Hub4/L3/OverruledShoreLimit": {"initial": None, "textformat": _n}, 1370 | } 1371 | ) 1372 | 1373 | paths_multiplus_dbus.update( 1374 | { 1375 | "/Hub4/Sustain": {"initial": 0, "textformat": _n}, 1376 | "/Hub4/TargetPowerIsMaxFeedIn": {"initial": 0, "textformat": _n}, 1377 | # ---- 1378 | # "/Interfaces/Mk2/Connection": {"initial": "/dev/ttyS3", "textformat": _n}, 1379 | # "/Interfaces/Mk2/ProductId": {"initial": 4464, "textformat": _n}, 1380 | # "/Interfaces/Mk2/ProductName": {"initial": "MK3", "textformat": _n}, 1381 | # "/Interfaces/Mk2/Status/Baudrate": {"initial": 115200, "textformat": _n}, 1382 | # "/Interfaces/Mk2/Status/BusFreeMode": {"initial": 1, "textformat": _n}, 1383 | # "/Interfaces/Mk2/Tunnel": {"initial": None, "textformat": _n}, 1384 | # "/Interfaces/Mk2/Version": {"initial": 1170216, "textformat": _n}, 1385 | # ---- 1386 | "/Leds/Absorption": {"initial": 0, "textformat": _n}, 1387 | "/Leds/Bulk": {"initial": 0, "textformat": _n}, 1388 | "/Leds/Float": {"initial": 0, "textformat": _n}, 1389 | "/Leds/Inverter": {"initial": 0, "textformat": _n}, 1390 | "/Leds/LowBattery": {"initial": 0, "textformat": _n}, 1391 | "/Leds/Mains": {"initial": 1, "textformat": _n}, 1392 | "/Leds/Overload": {"initial": 0, "textformat": _n}, 1393 | "/Leds/Temperature": {"initial": 0, "textformat": _n}, 1394 | "/Mode": {"initial": 3, "textformat": _n}, 1395 | "/ModeIsAdjustable": {"initial": 1, "textformat": _n}, 1396 | "/PvInverter/Disable": {"initial": 1, "textformat": _n}, 1397 | "/Quirks": {"initial": 0, "textformat": _n}, 1398 | "/RedetectSystem": {"initial": 0, "textformat": _n}, 1399 | "/Settings/Alarm/System/GridLost": {"initial": 1, "textformat": _n}, 1400 | "/Settings/SystemSetup/AcInput1": {"initial": 1, "textformat": _n}, 1401 | "/Settings/SystemSetup/AcInput2": {"initial": 0, "textformat": _n}, 1402 | "/ShortIds": {"initial": 1, "textformat": _n}, 1403 | "/Soc": {"initial": None, "textformat": _percent}, 1404 | "/State": {"initial": 3, "textformat": _n}, 1405 | "/SystemReset": {"initial": None, "textformat": _n}, 1406 | "/VebusChargeState": {"initial": 1, "textformat": _n}, 1407 | "/VebusError": {"initial": 0, "textformat": _n}, 1408 | "/VebusMainState": {"initial": 9, "textformat": _n}, 1409 | "/VebusSetChargeState": {"initial": 0, "textformat": _n}, 1410 | # ---- 1411 | "/UpdateIndex": {"initial": 0, "textformat": _n}, 1412 | } 1413 | ) 1414 | 1415 | time_driver_started = int(time()) 1416 | 1417 | # has to be called before DbusMultiPlusEmulator() else it does not work 1418 | system_items, grid_items, ac_load_items = setup_dbus_external_items() 1419 | 1420 | dbus_multiplus_emulator = DbusMultiPlusEmulator( 1421 | servicename="com.victronenergy.vebus.ttyS3", 1422 | deviceinstance=275, 1423 | paths=paths_multiplus_dbus, 1424 | ) 1425 | 1426 | dbus_multiplus_emulator.system_items = system_items 1427 | dbus_multiplus_emulator.grid_items = grid_items 1428 | dbus_multiplus_emulator.ac_load_items = ac_load_items 1429 | 1430 | logging.info("Connected to dbus and switching over to GLib.MainLoop() (= event based)") 1431 | mainloop = GLib.MainLoop() 1432 | mainloop.run() 1433 | 1434 | 1435 | if __name__ == "__main__": 1436 | main() 1437 | -------------------------------------------------------------------------------- /dbus-multiplus-emulator/ext/velib_python/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Victron Energy BV 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /dbus-multiplus-emulator/ext/velib_python/README.md: -------------------------------------------------------------------------------- 1 | velib_python 2 | ============ 3 | 4 | [![Build Status](https://travis-ci.com/victronenergy/velib_python.svg?branch=master)](https://travis-ci.org/victronenergy/velib_python) 5 | 6 | This is the general python library within Victron. It contains code that is related to D-Bus and the Color 7 | Control GX. See http://www.victronenergy.com/panel-systems-remote-monitoring/colorcontrol/ for more 8 | infomation about that panel. 9 | 10 | Files busitem.py, dbusitem.py and tracing.py are deprecated. 11 | 12 | The main files are vedbus.py, dbusmonitor.py and settingsdevice.py. 13 | 14 | - Use VeDbusService to put your process on dbus and let other services interact with you. 15 | - Use VeDbusItemImport to read a single value from other processes the dbus, and monitor its signals. 16 | - Use DbusMonitor to monitor multiple values from other processes 17 | - Use SettingsDevice to store your settings in flash, via the com.victronenergy.settings dbus service. See 18 | https://github.com/victronenergy/localsettings for more info. 19 | 20 | Code style 21 | ========== 22 | 23 | Comply with PEP8, except: 24 | - use tabs instead of spaces, since we use tabs for all projects within Victron. 25 | - max line length = 110 26 | 27 | Run this command to set git diff to tabsize is 4 spaces. Replace --local with --global to do it globally for the current 28 | user account. 29 | 30 | git config --local core.pager 'less -x4' 31 | 32 | Run this command to check your code agains PEP8 33 | 34 | pep8 --max-line-length=110 --ignore=W191 *.py 35 | 36 | D-Bus 37 | ===== 38 | 39 | D-Bus is an/the inter process communication bus used on Linux for many things. Victron uses it on the CCGX to have all the different processes exchange data. Protocol drivers publish data read from products (for example battery voltage) on the D-Bus, and other processes (the GUI for example) takes it from the D-Bus to show it on the display. 40 | 41 | Libraries that implement D-Bus connectivity are available in many programming languages (C, Python, etc). There are also many commandline tools available to talk to a running process via D-bus. See for example the dbuscli (executeable name dbus): http://code.google.com/p/dbus-tools/wiki/DBusCli, and also dbus-monitor and dbus-send. 42 | 43 | There are two sides in using the D-Bus, putting information on it (exporting as service with objects) and reading/writing to a process exporting a service. Note that instead of reading with GetValue, you can also subscribe to receive a signal when datachanges. Doing this saves unncessary context-switches in most cases. 44 | 45 | To get an idea of how to publish data on the dbus, run the example: 46 | 47 | matthijs@matthijs-VirtualBox:~/dev/velib_python/examples$ python vedbusservice_example.py 48 | vedbusservice_example.py starting up 49 | /Position value is 5 50 | /Position value is now 10 51 | try changing our RPM by executing the following command from a terminal 52 | 53 | dbus-send --print-reply --dest=com.victronenergy.example /RPM com.victronenergy.BusItem.SetValue int32:1200 54 | Reply will be <> 0 for values > 1000: not accepted. And reply will be 0 for values < 1000: accepted. 55 | 56 | Leave that terminal open, start a second terminal, and interrogate above service from the commandline: 57 | 58 | matthijs@matthijs-VirtualBox:~/dev/velib_python/examples$ dbus 59 | org.freedesktop.DBus 60 | org.freedesktop.PowerManagement 61 | com.victronenergy.example 62 | org.xfce.Terminal5 63 | org.xfce.Xfconf 64 | [and many more services in which we are not interested] 65 | 66 | To get more details, add the servicename: 67 | 68 | matthijs@matthijs-VirtualBox:~/dev/velib_python/examples$ dbus com.victronenergy.example 69 | / 70 | /Float 71 | /Int 72 | /NegativeInt 73 | /Position 74 | /RPM 75 | /String 76 | 77 | And get the value for the position: 78 | 79 | matthijs@matthijs-VirtualBox:~/dev/velib_python/examples$ dbus com.victronenergy.example /RPM GetValue 80 | 100 81 | 82 | And setting the value is also possible, the % makes dbus evaluate what comes behind it, resulting in an int instead of the default (a string).: 83 | 84 | matthijs@matthijs-VirtualBox:~/dev/velib_python/examples$ dbus com.victronenergy.example /RPM SetValue %1 85 | 0 86 | 87 | In this example, the 0 indicates succes. When trying an unsupported value, 2000, this is what happens: 88 | 89 | matthijs@matthijs-VirtualBox:~/dev/velib_python/examples$ dbus com.victronenergy.example /RPM SetValue %2000 90 | 2 91 | 92 | Exporting services, and the object paths (/Float, /Position, /Group1/Value1, etcetera) is standard D-Bus functionality. At Victron we designed and implemented a D-Bus interface, called com.victronenergy.BusItem. Example showing all interfaces supported by an object: 93 | 94 | matthijs@matthijs-VirtualBox:~/dev/velib_python/examples$ dbus com.victronenergy.example /RPM 95 | Interface org.freedesktop.DBus.Introspectable: 96 | String Introspect() 97 | 98 | Interface com.victronenergy.BusItem: 99 | Int32 SetValue(Variant newvalue) 100 | String GetDescription(String language, Int32 length) 101 | String GetText() 102 | Variant GetValue() 103 | -------------------------------------------------------------------------------- /dbus-multiplus-emulator/ext/velib_python/dbusmonitor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | ## @package dbus_vrm 5 | # This code takes care of the D-Bus interface (not all of below is implemented yet): 6 | # - on startup it scans the dbus for services we know. For each known service found, it searches for 7 | # objects/paths we know. Everything we find is stored in items{}, and an event is registered: if a 8 | # value changes weĺl be notified and can pass that on to our owner. For example the vrmLogger. 9 | # we know. 10 | # - after startup, it continues to monitor the dbus: 11 | # 1) when services are added we do the same check on that 12 | # 2) when services are removed, we remove any items that we had that referred to that service 13 | # 3) if an existing services adds paths we update ourselves as well: on init, we make a 14 | # VeDbusItemImport for a non-, or not yet existing objectpaths as well1 15 | # 16 | # Code is used by the vrmLogger, and also the pubsub code. Both are other modules in the dbus_vrm repo. 17 | 18 | from dbus.mainloop.glib import DBusGMainLoop 19 | from gi.repository import GLib 20 | import dbus 21 | import dbus.service 22 | import inspect 23 | import logging 24 | import argparse 25 | import pprint 26 | import traceback 27 | import os 28 | from collections import defaultdict 29 | from functools import partial 30 | 31 | # our own packages 32 | from ve_utils import exit_on_error, wrap_dbus_value, unwrap_dbus_value, add_name_owner_changed_receiver 33 | notfound = object() # For lookups where None is a valid result 34 | 35 | logger = logging.getLogger(__name__) 36 | logger.setLevel(logging.INFO) 37 | class SystemBus(dbus.bus.BusConnection): 38 | def __new__(cls): 39 | return dbus.bus.BusConnection.__new__(cls, dbus.bus.BusConnection.TYPE_SYSTEM) 40 | 41 | class SessionBus(dbus.bus.BusConnection): 42 | def __new__(cls): 43 | return dbus.bus.BusConnection.__new__(cls, dbus.bus.BusConnection.TYPE_SESSION) 44 | 45 | class MonitoredValue(object): 46 | def __init__(self, value, text, options): 47 | super(MonitoredValue, self).__init__() 48 | self.value = value 49 | self.text = text 50 | self.options = options 51 | 52 | # For legacy code, allow treating this as a tuple/list 53 | def __iter__(self): 54 | return iter((self.value, self.text, self.options)) 55 | 56 | class Service(object): 57 | def __init__(self, id, serviceName, deviceInstance): 58 | super(Service, self).__init__() 59 | self.id = id 60 | self.name = serviceName 61 | self.paths = {} 62 | self._seen = set() 63 | self.deviceInstance = deviceInstance 64 | 65 | # For legacy code, attributes can still be accessed as if keys from a 66 | # dictionary. 67 | def __setitem__(self, key, value): 68 | self.__dict__[key] = value 69 | def __getitem__(self, key): 70 | return self.__dict__[key] 71 | 72 | def set_seen(self, path): 73 | self._seen.add(path) 74 | 75 | def seen(self, path): 76 | return path in self._seen 77 | 78 | @property 79 | def service_class(self): 80 | return '.'.join(self.name.split('.')[:3]) 81 | 82 | class DbusMonitor(object): 83 | ## Constructor 84 | def __init__(self, dbusTree, valueChangedCallback=None, deviceAddedCallback=None, 85 | deviceRemovedCallback=None, namespace="com.victronenergy", ignoreServices=[]): 86 | # valueChangedCallback is the callback that we call when something has changed. 87 | # def value_changed_on_dbus(dbusServiceName, dbusPath, options, changes, deviceInstance): 88 | # in which changes is a tuple with GetText() and GetValue() 89 | self.valueChangedCallback = valueChangedCallback 90 | self.deviceAddedCallback = deviceAddedCallback 91 | self.deviceRemovedCallback = deviceRemovedCallback 92 | self.dbusTree = dbusTree 93 | self.ignoreServices = ignoreServices 94 | 95 | # Lists all tracked services. Stores name, id, device instance, value per path, and whenToLog info 96 | # indexed by service name (eg. com.victronenergy.settings). 97 | self.servicesByName = {} 98 | 99 | # Same values as self.servicesByName, but indexed by service id (eg. :1.30) 100 | self.servicesById = {} 101 | 102 | # Keep track of services by class to speed up calls to get_service_list 103 | self.servicesByClass = defaultdict(list) 104 | 105 | # Keep track of any additional watches placed on items 106 | self.serviceWatches = defaultdict(list) 107 | 108 | # For a PC, connect to the SessionBus 109 | # For a CCGX, connect to the SystemBus 110 | self.dbusConn = SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else SystemBus() 111 | 112 | # subscribe to NameOwnerChange for bus connect / disconnect events. 113 | # NOTE: this is on a different bus then the one above! 114 | standardBus = (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ \ 115 | else dbus.SystemBus()) 116 | 117 | add_name_owner_changed_receiver(standardBus, self.dbus_name_owner_changed) 118 | 119 | # Subscribe to PropertiesChanged for all services 120 | self.dbusConn.add_signal_receiver(self.handler_value_changes, 121 | dbus_interface='com.victronenergy.BusItem', 122 | signal_name='PropertiesChanged', path_keyword='path', 123 | sender_keyword='senderId') 124 | 125 | # Subscribe to ItemsChanged for all services 126 | self.dbusConn.add_signal_receiver(self.handler_item_changes, 127 | dbus_interface='com.victronenergy.BusItem', 128 | signal_name='ItemsChanged', path='/', 129 | sender_keyword='senderId') 130 | 131 | logger.info('===== Search on dbus for services that we will monitor starting... =====') 132 | serviceNames = self.dbusConn.list_names() 133 | for serviceName in serviceNames: 134 | self.scan_dbus_service(serviceName) 135 | 136 | logger.info('===== Search on dbus for services that we will monitor finished =====') 137 | 138 | @staticmethod 139 | def make_service(serviceId, serviceName, deviceInstance): 140 | """ Override this to use a different kind of service object. """ 141 | return Service(serviceId, serviceName, deviceInstance) 142 | 143 | def make_monitor(self, service, path, value, text, options): 144 | """ Override this to do more things with monitoring. """ 145 | return MonitoredValue(unwrap_dbus_value(value), unwrap_dbus_value(text), options) 146 | 147 | def dbus_name_owner_changed(self, name, oldowner, newowner): 148 | if not name.startswith("com.victronenergy."): 149 | return 150 | 151 | #decouple, and process in main loop 152 | GLib.idle_add(exit_on_error, self._process_name_owner_changed, name, oldowner, newowner) 153 | 154 | def _process_name_owner_changed(self, name, oldowner, newowner): 155 | if newowner != '': 156 | # so we found some new service. Check if we can do something with it. 157 | newdeviceadded = self.scan_dbus_service(name) 158 | if newdeviceadded and self.deviceAddedCallback is not None: 159 | self.deviceAddedCallback(name, self.get_device_instance(name)) 160 | 161 | elif name in self.servicesByName: 162 | # it disappeared, we need to remove it. 163 | logger.info("%s disappeared from the dbus. Removing it from our lists" % name) 164 | service = self.servicesByName[name] 165 | del self.servicesById[service.id] 166 | del self.servicesByName[name] 167 | for watch in self.serviceWatches[name]: 168 | watch.remove() 169 | del self.serviceWatches[name] 170 | self.servicesByClass[service.service_class].remove(service) 171 | if self.deviceRemovedCallback is not None: 172 | self.deviceRemovedCallback(name, service.deviceInstance) 173 | 174 | def scan_dbus_service(self, serviceName): 175 | try: 176 | return self.scan_dbus_service_inner(serviceName) 177 | except: 178 | logger.error("Ignoring %s because of error while scanning:" % (serviceName)) 179 | traceback.print_exc() 180 | return False 181 | 182 | # Errors 'org.freedesktop.DBus.Error.ServiceUnknown' and 183 | # 'org.freedesktop.DBus.Error.Disconnected' seem to happen when the service 184 | # disappears while its being scanned. Which might happen, but is not really 185 | # normal either, so letting them go into the logs. 186 | 187 | # Scans the given dbus service to see if it contains anything interesting for us. If it does, add 188 | # it to our list of monitored D-Bus services. 189 | def scan_dbus_service_inner(self, serviceName): 190 | 191 | # make it a normal string instead of dbus string 192 | serviceName = str(serviceName) 193 | 194 | if (len(self.ignoreServices) != 0 and any(serviceName.startswith(x) for x in self.ignoreServices)): 195 | logger.debug("Ignoring service %s" % serviceName) 196 | return False 197 | 198 | paths = self.dbusTree.get('.'.join(serviceName.split('.')[0:3]), None) 199 | if paths is None: 200 | logger.debug("Ignoring service %s, not in the tree" % serviceName) 201 | return False 202 | 203 | logger.info("Found: %s, scanning and storing items" % serviceName) 204 | serviceId = self.dbusConn.get_name_owner(serviceName) 205 | 206 | # we should never be notified to add a D-Bus service that we already have. If this assertion 207 | # raises, check process_name_owner_changed, and D-Bus workings. 208 | assert serviceName not in self.servicesByName 209 | assert serviceId not in self.servicesById 210 | 211 | # Try to fetch everything with a GetItems, then fall back to older 212 | # methods if that fails 213 | try: 214 | values = self.dbusConn.call_blocking(serviceName, '/', None, 'GetItems', '', []) 215 | except dbus.exceptions.DBusException: 216 | logger.info("GetItems failed, trying legacy methods") 217 | else: 218 | return self.scan_dbus_service_getitems_done(serviceName, serviceId, values) 219 | 220 | if serviceName == 'com.victronenergy.settings': 221 | di = 0 222 | elif serviceName.startswith('com.victronenergy.vecan.'): 223 | di = 0 224 | else: 225 | try: 226 | di = self.dbusConn.call_blocking(serviceName, 227 | '/DeviceInstance', None, 'GetValue', '', []) 228 | except dbus.exceptions.DBusException: 229 | logger.info(" %s was skipped because it has no device instance" % serviceName) 230 | return False # Skip it 231 | else: 232 | di = int(di) 233 | 234 | logger.info(" %s has device instance %s" % (serviceName, di)) 235 | service = self.make_service(serviceId, serviceName, di) 236 | 237 | # Let's try to fetch everything in one go 238 | values = {} 239 | texts = {} 240 | try: 241 | values.update(self.dbusConn.call_blocking(serviceName, '/', None, 'GetValue', '', [])) 242 | texts.update(self.dbusConn.call_blocking(serviceName, '/', None, 'GetText', '', [])) 243 | except: 244 | pass 245 | 246 | for path, options in paths.items(): 247 | # path will be the D-Bus path: '/Ac/ActiveIn/L1/V' 248 | # options will be a dictionary: {'code': 'V', 'whenToLog': 'onIntervalAlways'} 249 | 250 | # Try to obtain the value we want from our bulk fetch. If we 251 | # cannot find it there, do an individual query. 252 | value = values.get(path[1:], notfound) 253 | if value != notfound: 254 | service.set_seen(path) 255 | text = texts.get(path[1:], notfound) 256 | if value is notfound or text is notfound: 257 | try: 258 | value = self.dbusConn.call_blocking(serviceName, path, None, 'GetValue', '', []) 259 | service.set_seen(path) 260 | text = self.dbusConn.call_blocking(serviceName, path, None, 'GetText', '', []) 261 | except dbus.exceptions.DBusException as e: 262 | if e.get_dbus_name() in ( 263 | 'org.freedesktop.DBus.Error.ServiceUnknown', 264 | 'org.freedesktop.DBus.Error.Disconnected'): 265 | raise # This exception will be handled below 266 | 267 | # TODO org.freedesktop.DBus.Error.UnknownMethod really 268 | # shouldn't happen but sometimes does. 269 | logger.debug("%s %s does not exist (yet)" % (serviceName, path)) 270 | value = None 271 | text = None 272 | 273 | service.paths[path] = self.make_monitor(service, path, unwrap_dbus_value(value), unwrap_dbus_value(text), options) 274 | 275 | 276 | logger.debug("Finished scanning and storing items for %s" % serviceName) 277 | 278 | # Adjust self at the end of the scan, so we don't have an incomplete set of 279 | # data if an exception occurs during the scan. 280 | self.servicesByName[serviceName] = service 281 | self.servicesById[serviceId] = service 282 | self.servicesByClass[service.service_class].append(service) 283 | 284 | return True 285 | 286 | def scan_dbus_service_getitems_done(self, serviceName, serviceId, values): 287 | # Keeping these exceptions for legacy reasons 288 | if serviceName == 'com.victronenergy.settings': 289 | di = 0 290 | elif serviceName.startswith('com.victronenergy.vecan.'): 291 | di = 0 292 | else: 293 | try: 294 | di = values['/DeviceInstance']['Value'] 295 | except KeyError: 296 | logger.info(" %s was skipped because it has no device instance" % serviceName) 297 | return False 298 | else: 299 | di = int(di) 300 | 301 | logger.info(" %s has device instance %s" % (serviceName, di)) 302 | service = self.make_service(serviceId, serviceName, di) 303 | 304 | paths = self.dbusTree.get('.'.join(serviceName.split('.')[0:3]), {}) 305 | for path, options in paths.items(): 306 | item = values.get(path, notfound) 307 | if item is notfound: 308 | service.paths[path] = self.make_monitor(service, path, None, None, options) 309 | else: 310 | service.set_seen(path) 311 | value = item.get('Value', None) 312 | text = item.get('Text', None) 313 | service.paths[path] = self.make_monitor(service, path, unwrap_dbus_value(value), unwrap_dbus_value(text), options) 314 | 315 | self.servicesByName[serviceName] = service 316 | self.servicesById[serviceId] = service 317 | self.servicesByClass[service.service_class].append(service) 318 | return True 319 | 320 | def handler_item_changes(self, items, senderId): 321 | if not isinstance(items, dict): 322 | return 323 | 324 | try: 325 | service = self.servicesById[senderId] 326 | except KeyError: 327 | # senderId isn't there, which means it hasn't been scanned yet. 328 | return 329 | 330 | for path, changes in items.items(): 331 | try: 332 | v = unwrap_dbus_value(changes['Value']) 333 | except (KeyError, TypeError): 334 | continue 335 | 336 | try: 337 | t = changes['Text'] 338 | except KeyError: 339 | t = str(v) 340 | self._handler_value_changes(service, path, v, t) 341 | 342 | def handler_value_changes(self, changes, path, senderId): 343 | # If this properyChange does not involve a value, our work is done. 344 | if 'Value' not in changes: 345 | return 346 | 347 | try: 348 | service = self.servicesById[senderId] 349 | except KeyError: 350 | # senderId isn't there, which means it hasn't been scanned yet. 351 | return 352 | 353 | v = unwrap_dbus_value(changes['Value']) 354 | # Some services don't send Text with their PropertiesChanged events. 355 | try: 356 | t = changes['Text'] 357 | except KeyError: 358 | t = str(v) 359 | self._handler_value_changes(service, path, v, t) 360 | 361 | def _handler_value_changes(self, service, path, value, text): 362 | try: 363 | a = service.paths[path] 364 | except KeyError: 365 | # path isn't there, which means it hasn't been scanned yet. 366 | return 367 | 368 | service.set_seen(path) 369 | 370 | # First update our store to the new value 371 | if a.value == value: 372 | return 373 | 374 | a.value = value 375 | a.text = text 376 | 377 | # And do the rest of the processing in on the mainloop 378 | if self.valueChangedCallback is not None: 379 | GLib.idle_add(exit_on_error, self._execute_value_changes, service.name, path, { 380 | 'Value': value, 'Text': text}, a.options) 381 | 382 | def _execute_value_changes(self, serviceName, objectPath, changes, options): 383 | # double check that the service still exists, as it might have 384 | # disappeared between scheduling-for and executing this function. 385 | if serviceName not in self.servicesByName: 386 | return 387 | 388 | self.valueChangedCallback(serviceName, objectPath, 389 | options, changes, self.get_device_instance(serviceName)) 390 | 391 | # Gets the value for a certain servicename and path 392 | # The default_value is returned when: 393 | # 1. When the service doesn't exist. 394 | # 2. When the path asked for isn't being monitored. 395 | # 3. When the path exists, but has dbus-invalid, ie an empty byte array. 396 | # 4. When the path asked for is being monitored, but doesn't exist for that service. 397 | def get_value(self, serviceName, objectPath, default_value=None): 398 | service = self.servicesByName.get(serviceName, None) 399 | if service is None: 400 | return default_value 401 | 402 | value = service.paths.get(objectPath, None) 403 | if value is None or value.value is None: 404 | return default_value 405 | 406 | return value.value 407 | 408 | # returns if a dbus exists now, by doing a blocking dbus call. 409 | # Typically seen will be sufficient and doesn't need access to the dbus. 410 | def exists(self, serviceName, objectPath): 411 | try: 412 | self.dbusConn.call_blocking(serviceName, objectPath, None, 'GetValue', '', []) 413 | return True 414 | except dbus.exceptions.DBusException as e: 415 | return False 416 | 417 | # Returns if there ever was a successful GetValue or valueChanged event. 418 | # Unlike get_value this return True also if the actual value is invalid. 419 | # 420 | # Note: the path might no longer exists anymore, but that doesn't happen in 421 | # practice. If a service really wants to reconfigure itself typically it should 422 | # reconnect to the dbus which causes it to be rescanned and seen will be updated. 423 | # If it is really needed to know if a path still exists, use exists. 424 | def seen(self, serviceName, objectPath): 425 | try: 426 | return self.servicesByName[serviceName].seen(objectPath) 427 | except KeyError: 428 | return False 429 | 430 | # Sets the value for a certain servicename and path, returns the return value of the D-Bus SetValue 431 | # method. If the underlying item does not exist (the service does not exist, or the objectPath was not 432 | # registered) the function will return -1 433 | def set_value(self, serviceName, objectPath, value): 434 | # Check if the D-Bus object referenced by serviceName and objectPath is registered. There is no 435 | # necessity to do this, but it is in line with previous implementations which kept VeDbusItemImport 436 | # objects for registers items only. 437 | service = self.servicesByName.get(serviceName, None) 438 | if service is None: 439 | return -1 440 | if objectPath not in service.paths: 441 | return -1 442 | # We do not catch D-Bus exceptions here, because the previous implementation did not do that either. 443 | return self.dbusConn.call_blocking(serviceName, objectPath, 444 | dbus_interface='com.victronenergy.BusItem', 445 | method='SetValue', signature=None, 446 | args=[wrap_dbus_value(value)]) 447 | 448 | # Similar to set_value, but operates asynchronously 449 | def set_value_async(self, serviceName, objectPath, value, 450 | reply_handler=None, error_handler=None): 451 | service = self.servicesByName.get(serviceName, None) 452 | if service is not None: 453 | if objectPath in service.paths: 454 | self.dbusConn.call_async(serviceName, objectPath, 455 | dbus_interface='com.victronenergy.BusItem', 456 | method='SetValue', signature=None, 457 | args=[wrap_dbus_value(value)], 458 | reply_handler=reply_handler, error_handler=error_handler) 459 | return 460 | 461 | if error_handler is not None: 462 | error_handler(TypeError('Service or path not found, ' 463 | 'service=%s, path=%s' % (serviceName, objectPath))) 464 | 465 | # returns a dictionary, keys are the servicenames, value the instances 466 | # optionally use the classfilter to get only a certain type of services, for 467 | # example com.victronenergy.battery. 468 | def get_service_list(self, classfilter=None): 469 | if classfilter is None: 470 | return { servicename: service.deviceInstance \ 471 | for servicename, service in self.servicesByName.items() } 472 | 473 | if classfilter not in self.servicesByClass: 474 | return {} 475 | 476 | return { service.name: service.deviceInstance \ 477 | for service in self.servicesByClass[classfilter] } 478 | 479 | def get_device_instance(self, serviceName): 480 | return self.servicesByName[serviceName].deviceInstance 481 | 482 | def track_value(self, serviceName, objectPath, callback, *args, **kwargs): 483 | """ A DbusMonitor can watch specific service/path combos for changes 484 | so that it is not fully reliant on the global handler_value_changes 485 | in this class. Additional watches are deleted automatically when 486 | the service disappears from dbus. """ 487 | cb = partial(callback, *args, **kwargs) 488 | 489 | def root_tracker(items): 490 | # Check if objectPath in dict 491 | try: 492 | v = items[objectPath] 493 | _v = unwrap_dbus_value(v['Value']) 494 | except (KeyError, TypeError): 495 | return # not in this dict 496 | 497 | try: 498 | t = v['Text'] 499 | except KeyError: 500 | cb({'Value': _v }) 501 | else: 502 | cb({'Value': _v, 'Text': t}) 503 | 504 | # Track changes on the path, and also on root 505 | self.serviceWatches[serviceName].extend(( 506 | self.dbusConn.add_signal_receiver(cb, 507 | dbus_interface='com.victronenergy.BusItem', 508 | signal_name='PropertiesChanged', 509 | path=objectPath, bus_name=serviceName), 510 | self.dbusConn.add_signal_receiver(root_tracker, 511 | dbus_interface='com.victronenergy.BusItem', 512 | signal_name='ItemsChanged', 513 | path="/", bus_name=serviceName), 514 | )) 515 | 516 | 517 | # ====== ALL CODE BELOW THIS LINE IS PURELY FOR DEVELOPING THIS CLASS ====== 518 | 519 | # Example function that can be used as a starting point to use this code 520 | def value_changed_on_dbus(dbusServiceName, dbusPath, dict, changes, deviceInstance): 521 | logger.debug("0 ----------------") 522 | logger.debug("1 %s%s changed" % (dbusServiceName, dbusPath)) 523 | logger.debug("2 vrm dict : %s" % dict) 524 | logger.debug("3 changes-text: %s" % changes['Text']) 525 | logger.debug("4 changes-value: %s" % changes['Value']) 526 | logger.debug("5 deviceInstance: %s" % deviceInstance) 527 | logger.debug("6 - end") 528 | 529 | 530 | def nameownerchange(a, b): 531 | # used to find memory leaks in dbusmonitor and VeDbusItemImport 532 | import gc 533 | gc.collect() 534 | objects = gc.get_objects() 535 | print (len([o for o in objects if type(o).__name__ == 'VeDbusItemImport'])) 536 | print (len([o for o in objects if type(o).__name__ == 'SignalMatch'])) 537 | print (len(objects)) 538 | 539 | 540 | def print_values(dbusmonitor): 541 | a = dbusmonitor.get_value('wrongservice', '/DbusInvalid', default_value=1000) 542 | b = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/NotInTheMonitorList', default_value=1000) 543 | c = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/DbusInvalid', default_value=1000) 544 | d = dbusmonitor.get_value('com.victronenergy.dummyservice.ttyO1', '/NonExistingButMonitored', default_value=1000) 545 | 546 | print ("All should be 1000: Wrong Service: %s, NotInTheMonitorList: %s, DbusInvalid: %s, NonExistingButMonitored: %s" % (a, b, c, d)) 547 | return True 548 | 549 | # We have a mainloop, but that is just for developing this code. Normally above class & code is used from 550 | # some other class, such as vrmLogger or the pubsub Implementation. 551 | def main(): 552 | # Init logging 553 | logging.basicConfig(level=logging.DEBUG) 554 | logger.info(__file__ + " is starting up") 555 | 556 | # Have a mainloop, so we can send/receive asynchronous calls to and from dbus 557 | DBusGMainLoop(set_as_default=True) 558 | 559 | import os 560 | import sys 561 | sys.path.insert(1, os.path.join(os.path.dirname(__file__), '../../')) 562 | 563 | dummy = {'code': None, 'whenToLog': 'configChange', 'accessLevel': None} 564 | monitorlist = {'com.victronenergy.dummyservice': { 565 | '/Connected': dummy, 566 | '/ProductName': dummy, 567 | '/Mgmt/Connection': dummy, 568 | '/Dc/0/Voltage': dummy, 569 | '/Dc/0/Current': dummy, 570 | '/Dc/0/Temperature': dummy, 571 | '/Load/I': dummy, 572 | '/FirmwareVersion': dummy, 573 | '/DbusInvalid': dummy, 574 | '/NonExistingButMonitored': dummy}} 575 | 576 | d = DbusMonitor(monitorlist, value_changed_on_dbus, 577 | deviceAddedCallback=nameownerchange, deviceRemovedCallback=nameownerchange) 578 | 579 | GLib.timeout_add(1000, print_values, d) 580 | 581 | # Start and run the mainloop 582 | logger.info("Starting mainloop, responding on only events") 583 | mainloop = GLib.MainLoop() 584 | mainloop.run() 585 | 586 | if __name__ == "__main__": 587 | main() 588 | -------------------------------------------------------------------------------- /dbus-multiplus-emulator/ext/velib_python/ve_utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import sys 4 | from traceback import print_exc 5 | from os import _exit as os_exit 6 | from os import statvfs 7 | from subprocess import check_output, CalledProcessError 8 | import logging 9 | import dbus 10 | logger = logging.getLogger(__name__) 11 | 12 | VEDBUS_INVALID = dbus.Array([], signature=dbus.Signature('i'), variant_level=1) 13 | 14 | class NoVrmPortalIdError(Exception): 15 | pass 16 | 17 | # Use this function to make sure the code quits on an unexpected exception. Make sure to use it 18 | # when using GLib.idle_add and also GLib.timeout_add. 19 | # Without this, the code will just keep running, since GLib does not stop the mainloop on an 20 | # exception. 21 | # Example: GLib.idle_add(exit_on_error, myfunc, arg1, arg2) 22 | def exit_on_error(func, *args, **kwargs): 23 | try: 24 | return func(*args, **kwargs) 25 | except: 26 | try: 27 | print ('exit_on_error: there was an exception. Printing stacktrace will be tried and then exit') 28 | print_exc() 29 | except: 30 | pass 31 | 32 | # sys.exit() is not used, since that throws an exception, which does not lead to a program 33 | # halt when used in a dbus callback, see connection.py in the Python/Dbus libraries, line 230. 34 | os_exit(1) 35 | 36 | 37 | __vrm_portal_id = None 38 | def get_vrm_portal_id(): 39 | # The original definition of the VRM Portal ID is that it is the mac 40 | # address of the onboard- ethernet port (eth0), stripped from its colons 41 | # (:) and lower case. This may however differ between platforms. On Venus 42 | # the task is therefore deferred to /sbin/get-unique-id so that a 43 | # platform specific method can be easily defined. 44 | # 45 | # If /sbin/get-unique-id does not exist, then use the ethernet address 46 | # of eth0. This also handles the case where velib_python is used as a 47 | # package install on a Raspberry Pi. 48 | # 49 | # On a Linux host where the network interface may not be eth0, you can set 50 | # the VRM_IFACE environment variable to the correct name. 51 | 52 | global __vrm_portal_id 53 | 54 | if __vrm_portal_id: 55 | return __vrm_portal_id 56 | 57 | portal_id = None 58 | 59 | # First try the method that works if we don't have a data partition. This 60 | # will fail when the current user is not root. 61 | try: 62 | portal_id = check_output("/sbin/get-unique-id").decode("utf-8", "ignore").strip() 63 | if not portal_id: 64 | raise NoVrmPortalIdError("get-unique-id returned blank") 65 | __vrm_portal_id = portal_id 66 | return portal_id 67 | except CalledProcessError: 68 | # get-unique-id returned non-zero 69 | raise NoVrmPortalIdError("get-unique-id returned non-zero") 70 | except OSError: 71 | # File doesn't exist, use fallback 72 | pass 73 | 74 | # Fall back to getting our id using a syscall. Assume we are on linux. 75 | # Allow the user to override what interface is used using an environment 76 | # variable. 77 | import fcntl, socket, struct, os 78 | 79 | iface = os.environ.get('VRM_IFACE', 'eth0').encode('ascii') 80 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 81 | try: 82 | info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', iface[:15])) 83 | except IOError: 84 | raise NoVrmPortalIdError("ioctl failed for eth0") 85 | 86 | __vrm_portal_id = info[18:24].hex() 87 | return __vrm_portal_id 88 | 89 | 90 | # See VE.Can registers - public.docx for definition of this conversion 91 | def convert_vreg_version_to_readable(version): 92 | def str_to_arr(x, length): 93 | a = [] 94 | for i in range(0, len(x), length): 95 | a.append(x[i:i+length]) 96 | return a 97 | 98 | x = "%x" % version 99 | x = x.upper() 100 | 101 | if len(x) == 5 or len(x) == 3 or len(x) == 1: 102 | x = '0' + x 103 | 104 | a = str_to_arr(x, 2); 105 | 106 | # remove the first 00 if there are three bytes and it is 00 107 | if len(a) == 3 and a[0] == '00': 108 | a.remove(0); 109 | 110 | # if we have two or three bytes now, and the first character is a 0, remove it 111 | if len(a) >= 2 and a[0][0:1] == '0': 112 | a[0] = a[0][1]; 113 | 114 | result = '' 115 | for item in a: 116 | result += ('.' if result != '' else '') + item 117 | 118 | 119 | result = 'v' + result 120 | 121 | return result 122 | 123 | 124 | def get_free_space(path): 125 | result = -1 126 | 127 | try: 128 | s = statvfs(path) 129 | result = s.f_frsize * s.f_bavail # Number of free bytes that ordinary users 130 | except Exception as ex: 131 | logger.info("Error while retrieving free space for path %s: %s" % (path, ex)) 132 | 133 | return result 134 | 135 | 136 | def _get_sysfs_machine_name(): 137 | try: 138 | with open('/sys/firmware/devicetree/base/model', 'r') as f: 139 | return f.read().rstrip('\x00') 140 | except IOError: 141 | pass 142 | 143 | return None 144 | 145 | # Returns None if it cannot find a machine name. Otherwise returns the string 146 | # containing the name 147 | def get_machine_name(): 148 | # First try calling the venus utility script 149 | try: 150 | return check_output("/usr/bin/product-name").strip().decode('UTF-8') 151 | except (CalledProcessError, OSError): 152 | pass 153 | 154 | # Fall back to sysfs 155 | name = _get_sysfs_machine_name() 156 | if name is not None: 157 | return name 158 | 159 | # Fall back to venus build machine name 160 | try: 161 | with open('/etc/venus/machine', 'r', encoding='UTF-8') as f: 162 | return f.read().strip() 163 | except IOError: 164 | pass 165 | 166 | return None 167 | 168 | 169 | def get_product_id(): 170 | """ Find the machine ID and return it. """ 171 | 172 | # First try calling the venus utility script 173 | try: 174 | return check_output("/usr/bin/product-id").strip().decode('UTF-8') 175 | except (CalledProcessError, OSError): 176 | pass 177 | 178 | # Fall back machine name mechanism 179 | name = _get_sysfs_machine_name() 180 | return { 181 | 'Color Control GX': 'C001', 182 | 'Venus GX': 'C002', 183 | 'Octo GX': 'C006', 184 | 'EasySolar-II': 'C007', 185 | 'MultiPlus-II': 'C008', 186 | 'Maxi GX': 'C009', 187 | 'Cerbo GX': 'C00A' 188 | }.get(name, 'C003') # C003 is Generic 189 | 190 | 191 | # Returns False if it cannot open the file. Otherwise returns its rstripped contents 192 | def read_file(path): 193 | content = False 194 | 195 | try: 196 | with open(path, 'r') as f: 197 | content = f.read().rstrip() 198 | except Exception as ex: 199 | logger.debug("Error while reading %s: %s" % (path, ex)) 200 | 201 | return content 202 | 203 | 204 | def wrap_dbus_value(value): 205 | if value is None: 206 | return VEDBUS_INVALID 207 | if isinstance(value, float): 208 | return dbus.Double(value, variant_level=1) 209 | if isinstance(value, bool): 210 | return dbus.Boolean(value, variant_level=1) 211 | if isinstance(value, int): 212 | try: 213 | return dbus.Int32(value, variant_level=1) 214 | except OverflowError: 215 | return dbus.Int64(value, variant_level=1) 216 | if isinstance(value, str): 217 | return dbus.String(value, variant_level=1) 218 | if isinstance(value, list): 219 | if len(value) == 0: 220 | # If the list is empty we cannot infer the type of the contents. So assume unsigned integer. 221 | # A (signed) integer is dangerous, because an empty list of signed integers is used to encode 222 | # an invalid value. 223 | return dbus.Array([], signature=dbus.Signature('u'), variant_level=1) 224 | return dbus.Array([wrap_dbus_value(x) for x in value], variant_level=1) 225 | if isinstance(value, dict): 226 | # Wrapping the keys of the dictionary causes D-Bus errors like: 227 | # 'arguments to dbus_message_iter_open_container() were incorrect, 228 | # assertion "(type == DBUS_TYPE_ARRAY && contained_signature && 229 | # *contained_signature == DBUS_DICT_ENTRY_BEGIN_CHAR) || (contained_signature == NULL || 230 | # _dbus_check_is_valid_signature (contained_signature))" failed in file ...' 231 | return dbus.Dictionary({(k, wrap_dbus_value(v)) for k, v in value.items()}, variant_level=1) 232 | return value 233 | 234 | 235 | dbus_int_types = (dbus.Int32, dbus.UInt32, dbus.Byte, dbus.Int16, dbus.UInt16, dbus.UInt32, dbus.Int64, dbus.UInt64) 236 | 237 | 238 | def unwrap_dbus_value(val): 239 | """Converts D-Bus values back to the original type. For example if val is of type DBus.Double, 240 | a float will be returned.""" 241 | if isinstance(val, dbus_int_types): 242 | return int(val) 243 | if isinstance(val, dbus.Double): 244 | return float(val) 245 | if isinstance(val, dbus.Array): 246 | v = [unwrap_dbus_value(x) for x in val] 247 | return None if len(v) == 0 else v 248 | if isinstance(val, (dbus.Signature, dbus.String)): 249 | return str(val) 250 | # Python has no byte type, so we convert to an integer. 251 | if isinstance(val, dbus.Byte): 252 | return int(val) 253 | if isinstance(val, dbus.ByteArray): 254 | return "".join([bytes(x) for x in val]) 255 | if isinstance(val, (list, tuple)): 256 | return [unwrap_dbus_value(x) for x in val] 257 | if isinstance(val, (dbus.Dictionary, dict)): 258 | # Do not unwrap the keys, see comment in wrap_dbus_value 259 | return dict([(x, unwrap_dbus_value(y)) for x, y in val.items()]) 260 | if isinstance(val, dbus.Boolean): 261 | return bool(val) 262 | return val 263 | 264 | # When supported, only name owner changes for the the given namespace are reported. This 265 | # prevents spending cpu time at irrelevant changes, like scripts accessing the bus temporarily. 266 | def add_name_owner_changed_receiver(dbus, name_owner_changed, namespace="com.victronenergy"): 267 | # support for arg0namespace is submitted upstream, but not included at the time of 268 | # writing, Venus OS does support it, so try if it works. 269 | if namespace is None: 270 | dbus.add_signal_receiver(name_owner_changed, signal_name='NameOwnerChanged') 271 | else: 272 | try: 273 | dbus.add_signal_receiver(name_owner_changed, 274 | signal_name='NameOwnerChanged', arg0namespace=namespace) 275 | except TypeError: 276 | dbus.add_signal_receiver(name_owner_changed, signal_name='NameOwnerChanged') 277 | -------------------------------------------------------------------------------- /dbus-multiplus-emulator/ext/velib_python/vedbus.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import dbus.service 5 | import logging 6 | import traceback 7 | import os 8 | import weakref 9 | from collections import defaultdict 10 | from ve_utils import wrap_dbus_value, unwrap_dbus_value 11 | 12 | # vedbus contains three classes: 13 | # VeDbusItemImport -> use this to read data from the dbus, ie import 14 | # VeDbusItemExport -> use this to export data to the dbus (one value) 15 | # VeDbusService -> use that to create a service and export several values to the dbus 16 | 17 | # Code for VeDbusItemImport is copied from busitem.py and thereafter modified. 18 | # All projects that used busitem.py need to migrate to this package. And some 19 | # projects used to define there own equivalent of VeDbusItemExport. Better to 20 | # use VeDbusItemExport, or even better the VeDbusService class that does it all for you. 21 | 22 | # TODOS 23 | # 1 check for datatypes, it works now, but not sure if all is compliant with 24 | # com.victronenergy.BusItem interface definition. See also the files in 25 | # tests_and_examples. And see 'if type(v) == dbus.Byte:' on line 102. Perhaps 26 | # something similar should also be done in VeDbusBusItemExport? 27 | # 2 Shouldn't VeDbusBusItemExport inherit dbus.service.Object? 28 | # 7 Make hard rules for services exporting data to the D-Bus, in order to make tracking 29 | # changes possible. Does everybody first invalidate its data before leaving the bus? 30 | # And what about before taking one object away from the bus, instead of taking the 31 | # whole service offline? 32 | # They should! And after taking one value away, do we need to know that someone left 33 | # the bus? Or we just keep that value in invalidated for ever? Result is that we can't 34 | # see the difference anymore between an invalidated value and a value that was first on 35 | # the bus and later not anymore. See comments above VeDbusItemImport as well. 36 | # 9 there are probably more todos in the code below. 37 | 38 | # Some thoughts with regards to the data types: 39 | # 40 | # Text from: http://dbus.freedesktop.org/doc/dbus-python/doc/tutorial.html#data-types 41 | # --- 42 | # Variants are represented by setting the variant_level keyword argument in the 43 | # constructor of any D-Bus data type to a value greater than 0 (variant_level 1 44 | # means a variant containing some other data type, variant_level 2 means a variant 45 | # containing a variant containing some other data type, and so on). If a non-variant 46 | # is passed as an argument but introspection indicates that a variant is expected, 47 | # it'll automatically be wrapped in a variant. 48 | # --- 49 | # 50 | # Also the different dbus datatypes, such as dbus.Int32, and dbus.UInt32 are a subclass 51 | # of Python int. dbus.String is a subclass of Python standard class unicode, etcetera 52 | # 53 | # So all together that explains why we don't need to explicitly convert back and forth 54 | # between the dbus datatypes and the standard python datatypes. Note that all datatypes 55 | # in python are objects. Even an int is an object. 56 | 57 | # The signature of a variant is 'v'. 58 | 59 | # Export ourselves as a D-Bus service. 60 | class VeDbusService(object): 61 | def __init__(self, servicename, bus=None, register=True): 62 | # dict containing the VeDbusItemExport objects, with their path as the key. 63 | self._dbusobjects = {} 64 | self._dbusnodes = {} 65 | self._ratelimiters = [] 66 | self._dbusname = None 67 | self.name = servicename 68 | 69 | # dict containing the onchange callbacks, for each object. Object path is the key 70 | self._onchangecallbacks = {} 71 | 72 | # Connect to session bus whenever present, else use the system bus 73 | self._dbusconn = bus or (dbus.SessionBus() if 'DBUS_SESSION_BUS_ADDRESS' in os.environ else dbus.SystemBus()) 74 | 75 | # make the dbus connection available to outside, could make this a true property instead, but ach.. 76 | self.dbusconn = self._dbusconn 77 | 78 | # Add the root item that will return all items as a tree 79 | self._dbusnodes['/'] = VeDbusRootExport(self._dbusconn, '/', self) 80 | 81 | # Immediately register the service unless requested not to 82 | if register: 83 | logging.warning("USING OUTDATED REGISTRATION METHOD!") 84 | logging.warning("Please set register=False, then call the register method " 85 | "after adding all mandatory paths. See " 86 | "https://github.com/victronenergy/venus/wiki/dbus-api") 87 | self.register() 88 | 89 | def register(self): 90 | # Register ourselves on the dbus, trigger an error if already in use (do_not_queue) 91 | self._dbusname = dbus.service.BusName(self.name, self._dbusconn, do_not_queue=True) 92 | logging.info("registered ourselves on D-Bus as %s" % self.name) 93 | 94 | # To force immediate deregistering of this dbus service and all its object paths, explicitly 95 | # call __del__(). 96 | def __del__(self): 97 | for node in list(self._dbusnodes.values()): 98 | node.__del__() 99 | self._dbusnodes.clear() 100 | for item in list(self._dbusobjects.values()): 101 | item.__del__() 102 | self._dbusobjects.clear() 103 | if self._dbusname: 104 | self._dbusname.__del__() # Forces call to self._bus.release_name(self._name), see source code 105 | self._dbusname = None 106 | 107 | def get_name(self): 108 | return self._dbusname.get_name() 109 | 110 | # @param callbackonchange function that will be called when this value is changed. First parameter will 111 | # be the path of the object, second the new value. This callback should return 112 | # True to accept the change, False to reject it. 113 | def add_path(self, path, value, description="", writeable=False, 114 | onchangecallback=None, gettextcallback=None, valuetype=None, itemtype=None): 115 | 116 | if onchangecallback is not None: 117 | self._onchangecallbacks[path] = onchangecallback 118 | 119 | itemtype = itemtype or VeDbusItemExport 120 | item = itemtype(self._dbusconn, path, value, description, writeable, 121 | self._value_changed, gettextcallback, deletecallback=self._item_deleted, valuetype=valuetype) 122 | 123 | spl = path.split('/') 124 | for i in range(2, len(spl)): 125 | subPath = '/'.join(spl[:i]) 126 | if subPath not in self._dbusnodes and subPath not in self._dbusobjects: 127 | self._dbusnodes[subPath] = VeDbusTreeExport(self._dbusconn, subPath, self) 128 | self._dbusobjects[path] = item 129 | logging.debug('added %s with start value %s. Writeable is %s' % (path, value, writeable)) 130 | return item 131 | 132 | # Add the mandatory paths, as per victron dbus api doc 133 | def add_mandatory_paths(self, processname, processversion, connection, 134 | deviceinstance, productid, productname, firmwareversion, hardwareversion, connected): 135 | self.add_path('/Mgmt/ProcessName', processname) 136 | self.add_path('/Mgmt/ProcessVersion', processversion) 137 | self.add_path('/Mgmt/Connection', connection) 138 | 139 | # Create rest of the mandatory objects 140 | self.add_path('/DeviceInstance', deviceinstance) 141 | self.add_path('/ProductId', productid) 142 | self.add_path('/ProductName', productname) 143 | self.add_path('/FirmwareVersion', firmwareversion) 144 | self.add_path('/HardwareVersion', hardwareversion) 145 | self.add_path('/Connected', connected) 146 | 147 | # Callback function that is called from the VeDbusItemExport objects when a value changes. This function 148 | # maps the change-request to the onchangecallback given to us for this specific path. 149 | def _value_changed(self, path, newvalue): 150 | if path not in self._onchangecallbacks: 151 | return True 152 | 153 | return self._onchangecallbacks[path](path, newvalue) 154 | 155 | def _item_deleted(self, path): 156 | self._dbusobjects.pop(path) 157 | for np in list(self._dbusnodes.keys()): 158 | if np != '/': 159 | for ip in self._dbusobjects: 160 | if ip.startswith(np + '/'): 161 | break 162 | else: 163 | self._dbusnodes[np].__del__() 164 | self._dbusnodes.pop(np) 165 | 166 | def __getitem__(self, path): 167 | return self._dbusobjects[path].local_get_value() 168 | 169 | def __setitem__(self, path, newvalue): 170 | self._dbusobjects[path].local_set_value(newvalue) 171 | 172 | def __delitem__(self, path): 173 | self._dbusobjects[path].__del__() # Invalidates and then removes the object path 174 | assert path not in self._dbusobjects 175 | 176 | def __contains__(self, path): 177 | return path in self._dbusobjects 178 | 179 | def __enter__(self): 180 | l = ServiceContext(self) 181 | self._ratelimiters.append(l) 182 | return l 183 | 184 | def __exit__(self, *exc): 185 | # pop off the top one and flush it. If with statements are nested 186 | # then each exit flushes its own part. 187 | if self._ratelimiters: 188 | self._ratelimiters.pop().flush() 189 | 190 | class ServiceContext(object): 191 | def __init__(self, parent): 192 | self.parent = parent 193 | self.changes = {} 194 | 195 | def __contains__(self, path): 196 | return path in self.parent 197 | 198 | def __getitem__(self, path): 199 | return self.parent[path] 200 | 201 | def __setitem__(self, path, newvalue): 202 | c = self.parent._dbusobjects[path]._local_set_value(newvalue) 203 | if c is not None: 204 | self.changes[path] = c 205 | 206 | def __delitem__(self, path): 207 | if path in self.changes: 208 | del self.changes[path] 209 | del self.parent[path] 210 | 211 | def flush(self): 212 | if self.changes: 213 | self.parent._dbusnodes['/'].ItemsChanged(self.changes) 214 | self.changes.clear() 215 | 216 | def add_path(self, path, value, *args, **kwargs): 217 | self.parent.add_path(path, value, *args, **kwargs) 218 | self.changes[path] = { 219 | 'Value': wrap_dbus_value(value), 220 | 'Text': self.parent._dbusobjects[path].GetText() 221 | } 222 | 223 | def del_tree(self, root): 224 | root = root.rstrip('/') 225 | for p in list(self.parent._dbusobjects.keys()): 226 | if p == root or p.startswith(root + '/'): 227 | self[p] = None 228 | self.parent._dbusobjects[p].__del__() 229 | 230 | def get_name(self): 231 | return self.parent.get_name() 232 | 233 | class TrackerDict(defaultdict): 234 | """ Same as defaultdict, but passes the key to default_factory. """ 235 | def __missing__(self, key): 236 | self[key] = x = self.default_factory(key) 237 | return x 238 | 239 | class VeDbusRootTracker(object): 240 | """ This tracks the root of a dbus path and listens for PropertiesChanged 241 | signals. When a signal arrives, parse it and unpack the key/value changes 242 | into traditional events, then pass it to the original eventCallback 243 | method. """ 244 | def __init__(self, bus, serviceName): 245 | self.importers = defaultdict(weakref.WeakSet) 246 | self.serviceName = serviceName 247 | self._match = bus.get_object(serviceName, '/', introspect=False).connect_to_signal( 248 | "ItemsChanged", weak_functor(self._items_changed_handler)) 249 | 250 | def __del__(self): 251 | self._match.remove() 252 | self._match = None 253 | 254 | def add(self, i): 255 | self.importers[i.path].add(i) 256 | 257 | def _items_changed_handler(self, items): 258 | if not isinstance(items, dict): 259 | return 260 | 261 | for path, changes in items.items(): 262 | try: 263 | v = changes['Value'] 264 | except KeyError: 265 | continue 266 | 267 | try: 268 | t = changes['Text'] 269 | except KeyError: 270 | t = str(unwrap_dbus_value(v)) 271 | 272 | for i in self.importers.get(path, ()): 273 | i._properties_changed_handler({'Value': v, 'Text': t}) 274 | 275 | """ 276 | Importing basics: 277 | - If when we power up, the D-Bus service does not exist, or it does exist and the path does not 278 | yet exist, still subscribe to a signal: as soon as it comes online it will send a signal with its 279 | initial value, which VeDbusItemImport will receive and use to update local cache. And, when set, 280 | call the eventCallback. 281 | - If when we power up, save it 282 | - When using get_value, know that there is no difference between services (or object paths) that don't 283 | exist and paths that are invalid (= empty array, see above). Both will return None. In case you do 284 | really want to know ifa path exists or not, use the exists property. 285 | - When a D-Bus service leaves the D-Bus, it will first invalidate all its values, and send signals 286 | with that update, and only then leave the D-Bus. (or do we need to subscribe to the NameOwnerChanged- 287 | signal!?!) To be discussed and make sure. Not really urgent, since all existing code that uses this 288 | class already subscribes to the NameOwnerChanged signal, and subsequently removes instances of this 289 | class. 290 | 291 | Read when using this class: 292 | Note that when a service leaves that D-Bus without invalidating all its exported objects first, for 293 | example because it is killed, VeDbusItemImport doesn't have a clue. So when using VeDbusItemImport, 294 | make sure to also subscribe to the NamerOwnerChanged signal on bus-level. Or just use dbusmonitor, 295 | because that takes care of all of that for you. 296 | """ 297 | class VeDbusItemImport(object): 298 | def __new__(cls, bus, serviceName, path, eventCallback=None, createsignal=True): 299 | instance = object.__new__(cls) 300 | 301 | # If signal tracking should be done, also add to root tracker 302 | if createsignal: 303 | if "_roots" not in cls.__dict__: 304 | cls._roots = TrackerDict(lambda k: VeDbusRootTracker(bus, k)) 305 | 306 | return instance 307 | 308 | ## Constructor 309 | # @param bus the bus-object (SESSION or SYSTEM). 310 | # @param serviceName the dbus-service-name (string), for example 'com.victronenergy.battery.ttyO1' 311 | # @param path the object-path, for example '/Dc/V' 312 | # @param eventCallback function that you want to be called on a value change 313 | # @param createSignal only set this to False if you use this function to one time read a value. When 314 | # leaving it to True, make sure to also subscribe to the NameOwnerChanged signal 315 | # elsewhere. See also note some 15 lines up. 316 | def __init__(self, bus, serviceName, path, eventCallback=None, createsignal=True): 317 | # TODO: is it necessary to store _serviceName and _path? Isn't it 318 | # stored in the bus_getobjectsomewhere? 319 | self._serviceName = serviceName 320 | self._path = path 321 | self._match = None 322 | # TODO: _proxy is being used in settingsdevice.py, make a getter for that 323 | self._proxy = bus.get_object(serviceName, path, introspect=False) 324 | self.eventCallback = eventCallback 325 | 326 | assert eventCallback is None or createsignal == True 327 | if createsignal: 328 | self._match = self._proxy.connect_to_signal( 329 | "PropertiesChanged", weak_functor(self._properties_changed_handler)) 330 | self._roots[serviceName].add(self) 331 | 332 | # store the current value in _cachedvalue. When it doesn't exists set _cachedvalue to 333 | # None, same as when a value is invalid 334 | self._cachedvalue = None 335 | try: 336 | v = self._proxy.GetValue() 337 | except dbus.exceptions.DBusException: 338 | pass 339 | else: 340 | self._cachedvalue = unwrap_dbus_value(v) 341 | 342 | def __del__(self): 343 | if self._match is not None: 344 | self._match.remove() 345 | self._match = None 346 | self._proxy = None 347 | 348 | def _refreshcachedvalue(self): 349 | self._cachedvalue = unwrap_dbus_value(self._proxy.GetValue()) 350 | 351 | ## Returns the path as a string, for example '/AC/L1/V' 352 | @property 353 | def path(self): 354 | return self._path 355 | 356 | ## Returns the dbus service name as a string, for example com.victronenergy.vebus.ttyO1 357 | @property 358 | def serviceName(self): 359 | return self._serviceName 360 | 361 | ## Returns the value of the dbus-item. 362 | # the type will be a dbus variant, for example dbus.Int32(0, variant_level=1) 363 | # this is not a property to keep the name consistant with the com.victronenergy.busitem interface 364 | # returns None when the property is invalid 365 | def get_value(self): 366 | return self._cachedvalue 367 | 368 | ## Writes a new value to the dbus-item 369 | def set_value(self, newvalue): 370 | r = self._proxy.SetValue(wrap_dbus_value(newvalue)) 371 | 372 | # instead of just saving the value, go to the dbus and get it. So we have the right type etc. 373 | if r == 0: 374 | self._refreshcachedvalue() 375 | 376 | return r 377 | 378 | ## Resets the item to its default value 379 | def set_default(self): 380 | self._proxy.SetDefault() 381 | self._refreshcachedvalue() 382 | 383 | ## Returns the text representation of the value. 384 | # For example when the value is an enum/int GetText might return the string 385 | # belonging to that enum value. Another example, for a voltage, GetValue 386 | # would return a float, 12.0Volt, and GetText could return 12 VDC. 387 | # 388 | # Note that this depends on how the dbus-producer has implemented this. 389 | def get_text(self): 390 | return self._proxy.GetText() 391 | 392 | ## Returns true of object path exists, and false if it doesn't 393 | @property 394 | def exists(self): 395 | # TODO: do some real check instead of this crazy thing. 396 | r = False 397 | try: 398 | r = self._proxy.GetValue() 399 | r = True 400 | except dbus.exceptions.DBusException: 401 | pass 402 | 403 | return r 404 | 405 | ## callback for the trigger-event. 406 | # @param eventCallback the event-callback-function. 407 | @property 408 | def eventCallback(self): 409 | return self._eventCallback 410 | 411 | @eventCallback.setter 412 | def eventCallback(self, eventCallback): 413 | self._eventCallback = eventCallback 414 | 415 | ## Is called when the value of the imported bus-item changes. 416 | # Stores the new value in our local cache, and calls the eventCallback, if set. 417 | def _properties_changed_handler(self, changes): 418 | if "Value" in changes: 419 | changes['Value'] = unwrap_dbus_value(changes['Value']) 420 | self._cachedvalue = changes['Value'] 421 | if self._eventCallback: 422 | # The reason behind this try/except is to prevent errors silently ending up the an error 423 | # handler in the dbus code. 424 | try: 425 | self._eventCallback(self._serviceName, self._path, changes) 426 | except: 427 | traceback.print_exc() 428 | os._exit(1) # sys.exit() is not used, since that also throws an exception 429 | 430 | 431 | class VeDbusTreeExport(dbus.service.Object): 432 | def __init__(self, bus, objectPath, service): 433 | dbus.service.Object.__init__(self, bus, objectPath) 434 | self._service = service 435 | logging.debug("VeDbusTreeExport %s has been created" % objectPath) 436 | 437 | def __del__(self): 438 | # self._get_path() will raise an exception when retrieved after the call to .remove_from_connection, 439 | # so we need a copy. 440 | path = self._get_path() 441 | if path is None: 442 | return 443 | self.remove_from_connection() 444 | logging.debug("VeDbusTreeExport %s has been removed" % path) 445 | 446 | def _get_path(self): 447 | if len(self._locations) == 0: 448 | return None 449 | return self._locations[0][1] 450 | 451 | def _get_value_handler(self, path, get_text=False): 452 | logging.debug("_get_value_handler called for %s" % path) 453 | r = {} 454 | px = path 455 | if not px.endswith('/'): 456 | px += '/' 457 | for p, item in self._service._dbusobjects.items(): 458 | if p.startswith(px): 459 | v = item.GetText() if get_text else wrap_dbus_value(item.local_get_value()) 460 | r[p[len(px):]] = v 461 | logging.debug(r) 462 | return r 463 | 464 | @dbus.service.method('com.victronenergy.BusItem', out_signature='v') 465 | def GetValue(self): 466 | value = self._get_value_handler(self._get_path()) 467 | return dbus.Dictionary(value, signature=dbus.Signature('sv'), variant_level=1) 468 | 469 | @dbus.service.method('com.victronenergy.BusItem', out_signature='v') 470 | def GetText(self): 471 | return self._get_value_handler(self._get_path(), True) 472 | 473 | def local_get_value(self): 474 | return self._get_value_handler(self.path) 475 | 476 | class VeDbusRootExport(VeDbusTreeExport): 477 | @dbus.service.signal('com.victronenergy.BusItem', signature='a{sa{sv}}') 478 | def ItemsChanged(self, changes): 479 | pass 480 | 481 | @dbus.service.method('com.victronenergy.BusItem', out_signature='a{sa{sv}}') 482 | def GetItems(self): 483 | return { 484 | path: { 485 | 'Value': wrap_dbus_value(item.local_get_value()), 486 | 'Text': item.GetText() } 487 | for path, item in self._service._dbusobjects.items() 488 | } 489 | 490 | 491 | class VeDbusItemExport(dbus.service.Object): 492 | ## Constructor of VeDbusItemExport 493 | # 494 | # Use this object to export (publish), values on the dbus 495 | # Creates the dbus-object under the given dbus-service-name. 496 | # @param bus The dbus object. 497 | # @param objectPath The dbus-object-path. 498 | # @param value Value to initialize ourselves with, defaults to None which means Invalid 499 | # @param description String containing a description. Can be called over the dbus with GetDescription() 500 | # @param writeable what would this do!? :). 501 | # @param callback Function that will be called when someone else changes the value of this VeBusItem 502 | # over the dbus. First parameter passed to callback will be our path, second the new 503 | # value. This callback should return True to accept the change, False to reject it. 504 | def __init__(self, bus, objectPath, value=None, description=None, writeable=False, 505 | onchangecallback=None, gettextcallback=None, deletecallback=None, 506 | valuetype=None): 507 | dbus.service.Object.__init__(self, bus, objectPath) 508 | self._onchangecallback = onchangecallback 509 | self._gettextcallback = gettextcallback 510 | self._value = value 511 | self._description = description 512 | self._writeable = writeable 513 | self._deletecallback = deletecallback 514 | self._type = valuetype 515 | 516 | # To force immediate deregistering of this dbus object, explicitly call __del__(). 517 | def __del__(self): 518 | # self._get_path() will raise an exception when retrieved after the 519 | # call to .remove_from_connection, so we need a copy. 520 | path = self._get_path() 521 | if path == None: 522 | return 523 | if self._deletecallback is not None: 524 | self._deletecallback(path) 525 | self.remove_from_connection() 526 | logging.debug("VeDbusItemExport %s has been removed" % path) 527 | 528 | def _get_path(self): 529 | if len(self._locations) == 0: 530 | return None 531 | return self._locations[0][1] 532 | 533 | ## Sets the value. And in case the value is different from what it was, a signal 534 | # will be emitted to the dbus. This function is to be used in the python code that 535 | # is using this class to export values to the dbus. 536 | # set value to None to indicate that it is Invalid 537 | def local_set_value(self, newvalue): 538 | changes = self._local_set_value(newvalue) 539 | if changes is not None: 540 | self.PropertiesChanged(changes) 541 | 542 | def _local_set_value(self, newvalue): 543 | if self._value == newvalue: 544 | return None 545 | 546 | self._value = newvalue 547 | return { 548 | 'Value': wrap_dbus_value(newvalue), 549 | 'Text': self.GetText() 550 | } 551 | 552 | def local_get_value(self): 553 | return self._value 554 | 555 | # ==== ALL FUNCTIONS BELOW THIS LINE WILL BE CALLED BY OTHER PROCESSES OVER THE DBUS ==== 556 | 557 | ## Dbus exported method SetValue 558 | # Function is called over the D-Bus by other process. It will first check (via callback) if new 559 | # value is accepted. And it is, stores it and emits a changed-signal. 560 | # @param value The new value. 561 | # @return completion-code When successful a 0 is return, and when not a -1 is returned. 562 | @dbus.service.method('com.victronenergy.BusItem', in_signature='v', out_signature='i') 563 | def SetValue(self, newvalue): 564 | if not self._writeable: 565 | return 1 # NOT OK 566 | 567 | newvalue = unwrap_dbus_value(newvalue) 568 | 569 | # If value type is enforced, cast it. If the type can be coerced 570 | # python will do it for us. This allows ints to become floats, 571 | # or bools to become ints. Additionally also allow None, so that 572 | # a path may be invalidated. 573 | if self._type is not None and newvalue is not None: 574 | try: 575 | newvalue = self._type(newvalue) 576 | except (ValueError, TypeError): 577 | return 1 # NOT OK 578 | 579 | if newvalue == self._value: 580 | return 0 # OK 581 | 582 | # call the callback given to us, and check if new value is OK. 583 | if (self._onchangecallback is None or 584 | (self._onchangecallback is not None and self._onchangecallback(self.__dbus_object_path__, newvalue))): 585 | 586 | self.local_set_value(newvalue) 587 | return 0 # OK 588 | 589 | return 2 # NOT OK 590 | 591 | ## Dbus exported method GetDescription 592 | # 593 | # Returns the a description. 594 | # @param language A language code (e.g. ISO 639-1 en-US). 595 | # @param length Lenght of the language string. 596 | # @return description 597 | @dbus.service.method('com.victronenergy.BusItem', in_signature='si', out_signature='s') 598 | def GetDescription(self, language, length): 599 | return self._description if self._description is not None else 'No description given' 600 | 601 | ## Dbus exported method GetValue 602 | # Returns the value. 603 | # @return the value when valid, and otherwise an empty array 604 | @dbus.service.method('com.victronenergy.BusItem', out_signature='v') 605 | def GetValue(self): 606 | return wrap_dbus_value(self._value) 607 | 608 | ## Dbus exported method GetText 609 | # Returns the value as string of the dbus-object-path. 610 | # @return text A text-value. '---' when local value is invalid 611 | @dbus.service.method('com.victronenergy.BusItem', out_signature='s') 612 | def GetText(self): 613 | if self._value is None: 614 | return '---' 615 | 616 | # Default conversion from dbus.Byte will get you a character (so 'T' instead of '84'), so we 617 | # have to convert to int first. Note that if a dbus.Byte turns up here, it must have come from 618 | # the application itself, as all data from the D-Bus should have been unwrapped by now. 619 | if self._gettextcallback is None and type(self._value) == dbus.Byte: 620 | return str(int(self._value)) 621 | 622 | if self._gettextcallback is None and self.__dbus_object_path__ == '/ProductId': 623 | return "0x%X" % self._value 624 | 625 | if self._gettextcallback is None: 626 | return str(self._value) 627 | 628 | return self._gettextcallback(self.__dbus_object_path__, self._value) 629 | 630 | ## The signal that indicates that the value has changed. 631 | # Other processes connected to this BusItem object will have subscribed to the 632 | # event when they want to track our state. 633 | @dbus.service.signal('com.victronenergy.BusItem', signature='a{sv}') 634 | def PropertiesChanged(self, changes): 635 | pass 636 | 637 | ## This class behaves like a regular reference to a class method (eg. self.foo), but keeps a weak reference 638 | ## to the object which method is to be called. 639 | ## Use this object to break circular references. 640 | class weak_functor: 641 | def __init__(self, f): 642 | self._r = weakref.ref(f.__self__) 643 | self._f = weakref.ref(f.__func__) 644 | 645 | def __call__(self, *args, **kargs): 646 | r = self._r() 647 | f = self._f() 648 | if r == None or f == None: 649 | return 650 | f(r, *args, **kargs) 651 | -------------------------------------------------------------------------------- /dbus-multiplus-emulator/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 3 | SERVICE_NAME=$(basename $SCRIPT_DIR) 4 | 5 | # set permissions for script files 6 | chmod 744 $SCRIPT_DIR/$SERVICE_NAME.py 7 | chmod 744 $SCRIPT_DIR/install.sh 8 | chmod 744 $SCRIPT_DIR/restart.sh 9 | chmod 744 $SCRIPT_DIR/uninstall.sh 10 | chmod 755 $SCRIPT_DIR/service/run 11 | chmod 755 $SCRIPT_DIR/service/log/run 12 | 13 | # create sym-link to run script in deamon 14 | ln -s $SCRIPT_DIR/service /service/$SERVICE_NAME 15 | 16 | # add install-script to rc.local to be ready for firmware update 17 | filename=/data/rc.local 18 | if [ ! -f $filename ] 19 | then 20 | touch $filename 21 | chmod 777 $filename 22 | echo "#!/bin/bash" >> $filename 23 | echo >> $filename 24 | fi 25 | 26 | # if not alreay added, then add to rc.local 27 | grep -qxF "bash $SCRIPT_DIR/install.sh" $filename || echo "bash $SCRIPT_DIR/install.sh" >> $filename 28 | 29 | # set needed dbus settings 30 | dbus -y com.victronenergy.settings /Settings AddSetting Alarm/System GridLost 1 i 0 2 > /dev/null 31 | dbus -y com.victronenergy.settings /Settings AddSetting CanBms/SocketcanCan0 CustomName '' s 0 2 > /dev/null 32 | dbus -y com.victronenergy.settings /Settings AddSetting CanBms/SocketcanCan0 ProductId 0 i 0 9999 > /dev/null 33 | dbus -y com.victronenergy.settings /Settings AddSetting Canbus/can0 Profile 0 i 0 9999 > /dev/null 34 | dbus -y com.victronenergy.settings /Settings AddSetting SystemSetup AcInput1 1 i 0 2 > /dev/null 35 | dbus -y com.victronenergy.settings /Settings AddSetting SystemSetup AcInput2 0 i 0 2 > /dev/null 36 | -------------------------------------------------------------------------------- /dbus-multiplus-emulator/restart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 3 | SERVICE_NAME=$(basename $SCRIPT_DIR) 4 | 5 | kill $(pgrep -f "python $SCRIPT_DIR/$SERVICE_NAME.py") 6 | -------------------------------------------------------------------------------- /dbus-multiplus-emulator/service/log/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exec multilog t s25000 n4 /var/log/dbus-multiplus-emulator 3 | -------------------------------------------------------------------------------- /dbus-multiplus-emulator/service/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo "*** starting dbus-multiplus-emulator ***" 3 | exec 2>&1 4 | exec python /data/etc/dbus-multiplus-emulator/dbus-multiplus-emulator.py 5 | -------------------------------------------------------------------------------- /dbus-multiplus-emulator/uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) 3 | SERVICE_NAME=$(basename $SCRIPT_DIR) 4 | 5 | sed -i "/$SERVICE_NAME/d" /data/rc.local 6 | rm /service/$SERVICE_NAME 7 | kill $(pgrep -f "supervise $SERVICE_NAME") 8 | 9 | $SCRIPT_DIR/restart.sh 10 | 11 | # remove settings 12 | echo "Do you want to remove the dbus entries added by this driver? (y/N)" 13 | read -r confirm 14 | if [ "$confirm" = "y" ] || [ "$confirm" = "Y" ]; then 15 | dbus -y com.victronenergy.settings /Settings RemoveSettings "%[ '/Alarm/System/GridLost', '/CanBms/SocketcanCan0/CustomName', '/CanBms/SocketcanCan0/ProductId', '/Canbus/can0/Profile', '/SystemSetup/AcInput1', '/SystemSetup/AcInput2' ]" 16 | fi 17 | -------------------------------------------------------------------------------- /download.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | driver_path="/data/etc" 4 | driver_name="dbus-multiplus-emulator" 5 | 6 | echo "" 7 | echo "" 8 | 9 | # fetch version numbers for different versions 10 | echo -n "Fetch current version numbers..." 11 | 12 | # latest release 13 | latest_release_stable=$(curl -s https://api.github.com/repos/mr-manuel/venus-os_${driver_name}/releases/latest | grep "tag_name" | cut -d : -f 2,3 | tr -d "\ " | tr -d \" | tr -d \,) 14 | 15 | # nightly build 16 | latest_release_nightly=$(curl -s https://raw.githubusercontent.com/mr-manuel/venus-os_${driver_name}/master/${driver_name}/${driver_name}.py | grep HardwareVersion | awk -F'"' '{print $4}') 17 | 18 | 19 | echo 20 | PS3=$'\nSelect which version you want to install and enter the corresponding number: ' 21 | 22 | # create list of versions 23 | version_list=( 24 | "latest release \"$latest_release_stable\"" 25 | "nightly build \"v$latest_release_nightly\"" 26 | "quit" 27 | ) 28 | 29 | select version in "${version_list[@]}" 30 | do 31 | case $version in 32 | "latest release \"$latest_release_stable\"") 33 | break 34 | ;; 35 | "nightly build \"v$latest_release_nightly\"") 36 | break 37 | ;; 38 | "quit") 39 | exit 0 40 | ;; 41 | *) 42 | echo "> Invalid option: $REPLY. Please enter a number!" 43 | ;; 44 | esac 45 | done 46 | 47 | echo "> Selected: $version" 48 | echo "" 49 | 50 | 51 | echo "" 52 | if [ -d ${driver_path}/${driver_name} ]; then 53 | echo "Updating driver '$driver_name' as '$driver_name'..." 54 | else 55 | echo "Installing driver '$driver_name' as '$driver_name'..." 56 | fi 57 | 58 | 59 | # change to temp folder 60 | cd /tmp 61 | 62 | 63 | # download driver 64 | echo "" 65 | echo "Downloading driver..." 66 | 67 | 68 | ## latest release 69 | if [ "$version" = "latest release \"$latest_release_stable\"" ]; then 70 | # download latest release 71 | url=$(curl -s https://api.github.com/repos/mr-manuel/venus-os_${driver_name}/releases/latest | grep "zipball_url" | sed -n 's/.*"zipball_url": "\([^"]*\)".*/\1/p') 72 | fi 73 | 74 | ## nightly build 75 | if [ "$version" = "nightly build \"v$latest_release_nightly\"" ]; then 76 | # download nightly build 77 | url="https://github.com/mr-manuel/venus-os_${driver_name}/archive/refs/heads/master.zip" 78 | fi 79 | 80 | echo "Downloading from: $url" 81 | wget -O /tmp/venus-os_${driver_name}.zip "$url" 82 | 83 | # check if download was successful 84 | if [ ! -f /tmp/venus-os_${driver_name}.zip ]; then 85 | echo "" 86 | echo "Download failed. Exiting..." 87 | exit 1 88 | fi 89 | 90 | 91 | # If updating: cleanup old folder 92 | if [ -d /tmp/venus-os_${driver_name}-master ]; then 93 | rm -rf /tmp/venus-os_${driver_name}-master 94 | fi 95 | 96 | 97 | # unzip folder 98 | echo "Unzipping driver..." 99 | unzip venus-os_${driver_name}.zip 100 | 101 | # Find and rename the extracted folder to be always the same 102 | extracted_folder=$(find /tmp/ -maxdepth 1 -type d -name "*${driver_name}-*") 103 | 104 | if [ -n "$extracted_folder" ]; then 105 | mv "$extracted_folder" /tmp/venus-os_${driver_name}-master 106 | else 107 | echo "Error: Could not find extracted folder. Exiting..." 108 | exit 1 109 | fi 110 | 111 | 112 | # If updating: backup existing config file 113 | if [ -f ${driver_path}/${driver_name}/config.ini ]; then 114 | echo "" 115 | echo "Backing up existing config file..." 116 | mv ${driver_path}/${driver_name}/config.ini ${driver_path}/${driver_name}_config.ini 117 | fi 118 | 119 | 120 | # If updating: cleanup existing driver 121 | if [ -d ${driver_path}/${driver_name} ]; then 122 | echo "" 123 | echo "Cleaning up existing driver..." 124 | rm -rf ${driver_path}/${driver_name} 125 | fi 126 | 127 | 128 | # copy files 129 | echo "" 130 | echo "Copying new driver files..." 131 | cp -R /tmp/venus-os_${driver_name}-master/${driver_name}/ ${driver_path}/${driver_name}/ 132 | 133 | # remove temp files 134 | echo "" 135 | echo "Cleaning up temp files..." 136 | rm -rf /tmp/venus-os_${driver_name}.zip 137 | rm -rf /tmp/venus-os_${driver_name}-master 138 | 139 | 140 | # If updating: restore existing config file 141 | if [ -f ${driver_path}/${driver_name}_config.ini ]; then 142 | echo "" 143 | echo "Restoring existing config file..." 144 | mv ${driver_path}/${driver_name}_config.ini ${driver_path}/${driver_name}/config.ini 145 | fi 146 | 147 | 148 | # set permissions for files 149 | echo "" 150 | echo "Setting permissions for files..." 151 | chmod 755 ${driver_path}/${driver_name}/${driver_name}.py 152 | chmod 755 ${driver_path}/${driver_name}/install.sh 153 | chmod 755 ${driver_path}/${driver_name}/restart.sh 154 | chmod 755 ${driver_path}/${driver_name}/uninstall.sh 155 | chmod 755 ${driver_path}/${driver_name}/service/run 156 | chmod 755 ${driver_path}/${driver_name}/service/log/run 157 | 158 | 159 | # copy default config file 160 | if [ ! -f ${driver_path}/${driver_name}/config.ini ]; then 161 | echo "" 162 | echo "" 163 | echo "First installation detected. Copying default config file..." 164 | echo "" 165 | echo "You can edit the config file with the following command:" 166 | echo "nano ${driver_path}/${driver_name}/config.ini" 167 | cp ${driver_path}/${driver_name}/config.sample.ini ${driver_path}/${driver_name}/config.ini 168 | echo "" 169 | echo "** Execute the install.sh script after you have edited the config file! **" 170 | echo "You can execute the install.sh script with the following command:" 171 | echo "bash ${driver_path}/${driver_name}/install.sh" 172 | echo "" 173 | else 174 | echo "" 175 | echo "Restaring driver to apply new version..." 176 | /bin/bash ${driver_path}/${driver_name}/restart.sh 177 | fi 178 | 179 | 180 | echo 181 | echo "Done." 182 | echo 183 | echo 184 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 216 3 | exclude = 'dbus-multiplus-emulator/ext' 4 | --------------------------------------------------------------------------------