diff --git a/battery_service.py b/battery_service.py index b8be83b..9aa37c9 100755 --- a/battery_service.py +++ b/battery_service.py @@ -13,6 +13,7 @@ from ve_utils import unwrap_dbus_value from dbusmonitor import DbusMonitor from settableservice import SettableService +from data_merger import DataMerger from collections import deque, namedtuple import math import functools @@ -236,65 +237,6 @@ def __init__(self, unit, triggerPaths=None, action=None): BATTERY_PATHS = {**AGGREGATED_BATTERY_PATHS, **ACTIVE_BATTERY_PATHS} -class DataMerger: - def __init__(self, config, service_name_resolver): - if isinstance(config, list): - # convert short-hand format - expanded_config = {service_name_resolver.resolve_service_name(serviceName): list(BATTERY_PATHS) for serviceName in config} - elif isinstance(config, dict): - expanded_config = {} - for k, v in config.items(): - if not v: - v = list(BATTERY_PATHS) - expanded_config[service_name_resolver.resolve_service_name(k)] = v - elif config is None: - expanded_config = {} - else: - raise ValueError(f"Unsupported config object: {type(config)}") - - self.service_names = list(expanded_config) - - self.data_by_path = {} - for service_name, path_list in expanded_config.items(): - for p in path_list: - path_values = self.data_by_path.get(p) - if path_values is None: - path_values = {} - self.data_by_path[p] = path_values - path_values[service_name] = None - - def init_values(self, service_name, api): - paths_changed = [] - for p, path_values in self.data_by_path.items(): - if service_name in path_values: - path_values[service_name] = api.get_value(service_name, p) - paths_changed.append(p) - return paths_changed - - def clear_values(self, service_name): - paths_changed = [] - for p, path_values in self.data_by_path.items(): - if service_name in path_values: - path_values[service_name] = None - paths_changed.append(p) - return paths_changed - - def update_service_value(self, service_name, path, value): - path_values = self.data_by_path.get(path) - if path_values: - if service_name in path_values: - path_values[service_name] = value - - def get_value(self, path): - path_values = self.data_by_path.get(path) - if path_values: - for service_name in self.service_names: - v = path_values.get(service_name) - if v is not None: - return v - return None - - class ServiceNameResolver: def __init__(self, conn): self.conn = conn @@ -405,8 +347,9 @@ def __init__(self, conn, serviceName, config): scanPaths.remove('/Capacity') serviceNameResolver = ServiceNameResolver(conn) - self._primaryServices = DataMerger(config.get("primaryServices"), serviceNameResolver) - self._auxiliaryServices = DataMerger(config.get("auxiliaryServices"), serviceNameResolver) + default_config_paths = list(BATTERY_PATHS) + self._primaryServices = DataMerger(config.get("primaryServices"), default_config_paths, serviceNameResolver) + self._auxiliaryServices = DataMerger(config.get("auxiliaryServices"), default_config_paths, serviceNameResolver) otherServiceNames = set() otherServiceNames.add("com.victronenergy.system") otherServiceNames.add("com.victronenergy.settings") @@ -861,7 +804,8 @@ def __init__(self, conn, serviceName, config, hook_config): if not is_battery_service_name(name) and not is_hook(name) and not is_name(name): raise ValueError(f"Invalid service name: {name}") - self._mergedServices = DataMerger(config, ServiceNameResolver(conn)) + default_config_paths = list(BATTERY_PATHS) + self._mergedServices = DataMerger(config, default_config_paths, ServiceNameResolver(conn)) self.hooks = [] for name in list(config): @@ -900,7 +844,7 @@ def register(self, timeout=0): paths_changed.update(changed) for hook in reversed(self.hooks): - changed = hook.init_values(self.monitor) + changed = hook.init_values() paths_changed.update(changed) self._batteries_changed() diff --git a/data_merger.py b/data_merger.py new file mode 100644 index 0000000..e1df2ed --- /dev/null +++ b/data_merger.py @@ -0,0 +1,59 @@ +from typing import Dict, List + +class DataMerger: + def __init__(self, config, default_config_paths: List[str], service_name_resolver): + if isinstance(config, list): + # convert short-hand format + expanded_config = {service_name_resolver.resolve_service_name(serviceName): default_config_paths for serviceName in config} + elif isinstance(config, dict): + expanded_config = {} + for k, v in config.items(): + if not v: + v = default_config_paths + expanded_config[service_name_resolver.resolve_service_name(k)] = v + elif config is None: + expanded_config = {} + else: + raise ValueError(f"Unsupported config object: {type(config)}") + + self.service_names: List[str] = list(expanded_config) + + self.data_by_path: Dict[str, Dict[str, str]] = {} + for service_name, path_list in expanded_config.items(): + for p in path_list: + path_values = self.data_by_path.get(p) + if path_values is None: + path_values = {} + self.data_by_path[p] = path_values + path_values[service_name] = None + + def init_values(self, service_name: str, api): + paths_changed = [] + for p, path_values in self.data_by_path.items(): + if service_name in path_values: + path_values[service_name] = api.get_value(service_name, p) + paths_changed.append(p) + return paths_changed + + def clear_values(self, service_name: str): + paths_changed = [] + for p, path_values in self.data_by_path.items(): + if service_name in path_values: + path_values[service_name] = None + paths_changed.append(p) + return paths_changed + + def update_service_value(self, service_name: str, path: str, value): + path_values = self.data_by_path.get(path) + if path_values: + if service_name in path_values: + path_values[service_name] = value + + def get_value(self, path: str): + path_values = self.data_by_path.get(path) + if path_values: + for service_name in self.service_names: + v = path_values.get(service_name) + if v is not None: + return v + return None diff --git a/hooks.py b/hooks.py index b2c763d..6133277 100644 --- a/hooks.py +++ b/hooks.py @@ -5,7 +5,7 @@ def __init__(self, service_name, merger, customName): self.customName = customName self.merger.data_by_path["/CustomName"] = {service_name: None} - def init_values(self, api): + def init_values(self): self.merger.update_service_value(self.service_name, "/CustomName", self.customName) return ["/CustomName"] @@ -20,32 +20,36 @@ def __init__(self, service_name, merger, ccls=None, dcls=None): self.ccls = ccls self.dcls = dcls - def init_values(self, api): - return [] + def init_values(self): + initial_voltage = self.merger.get_value("/Dc/0/Voltage") + return self.update_cls(initial_voltage) if initial_voltage is not None else [] def update_service_value(self, dbusServiceName, dbusPath, value): if dbusPath == "/Dc/0/Voltage": voltage = self.merger.get_value(dbusPath) - paths_changed = [] - - ccl = None - if self.ccls: - for v, cl in self.ccls.items(): - if voltage >= float(v): - ccl = float(cl) - if ccl is not None: - self.merger.update_service_value(self.service_name, "/Info/MaxChargeCurrent", ccl) - paths_changed.append("/Info/MaxChargeCurrent") - - dcl = None - if self.dcls: - for v, cl in self.dcls.items(): - if voltage >= float(v): - dcl = float(cl) - if dcl is not None: - self.merger.update_service_value(self.service_name, "/Info/MaxDischargeCurrent", dcl) - paths_changed.append("/Info/MaxDischargeCurrent") - - return paths_changed + return self.update_cls(voltage) else: return [] + + def update_cls(self, voltage): + paths_changed = [] + + ccl = None + if self.ccls: + for v, cl in self.ccls.items(): + if voltage >= float(v): + ccl = float(cl) + if ccl is not None: + self.merger.update_service_value(self.service_name, "/Info/MaxChargeCurrent", ccl) + paths_changed.append("/Info/MaxChargeCurrent") + + dcl = None + if self.dcls: + for v, cl in self.dcls.items(): + if voltage >= float(v): + dcl = float(cl) + if dcl is not None: + self.merger.update_service_value(self.service_name, "/Info/MaxDischargeCurrent", dcl) + paths_changed.append("/Info/MaxDischargeCurrent") + + return paths_changed diff --git a/tests/hooks_test.py b/tests/hooks_test.py new file mode 100644 index 0000000..4c5f592 --- /dev/null +++ b/tests/hooks_test.py @@ -0,0 +1,30 @@ +from data_merger import DataMerger +import hooks +import unittest + + +class SimpleServiceNameResolver: + def __init(self): + pass + + def resolve_service_name(self, name): + return name + + +class HooksTest(unittest.TestCase): + def test_custom_charging_hook(self): + battery_name = "com.victronenergy.battery.test" + merger = DataMerger(["class:hooks.CustomChargingHook", battery_name], ["/Dc/0/Voltage", "/Info/MaxChargeCurrent"], SimpleServiceNameResolver()) + config = { + "ccls": { + "13": 15, + "12": 10 + } + } + hook = hooks.CustomChargingHook("class:hooks.CustomChargingHook", merger, **config) + merger.update_service_value(battery_name, "/Info/MaxChargeCurrent", 20) + merger.update_service_value(battery_name, "/Dc/0/Voltage", 12.6) + paths_changed = hook.update_service_value(battery_name, "/Dc/0/Voltage", 12.6) + self.assertListEqual(paths_changed, ["/Info/MaxChargeCurrent"]) + self.assertEqual(merger.get_value("/Info/MaxChargeCurrent"), 10) + diff --git a/version b/version index cde1fbc..5a2f3db 100644 --- a/version +++ b/version @@ -1 +1 @@ -v3.6 +v3.7