├── .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 | [](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 |
--------------------------------------------------------------------------------