forked from Dr-Gigavolt/dbus-aggregate-batteries
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsettings.py
More file actions
282 lines (235 loc) · 11.9 KB
/
Copy pathsettings.py
File metadata and controls
282 lines (235 loc) · 11.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# -*- coding: utf-8 -*-
# Standard library importsimport bisect
import configparser
import logging
import sys
from pathlib import Path
from time import sleep
from typing import List, Any, Callable
PATH_CONFIG_DEFAULT: str = "config.default.ini"
PATH_CONFIG_USER: str = "config.ini"
config = configparser.ConfigParser()
path = Path(__file__).parents[0]
default_config_file_path = str(path.joinpath(PATH_CONFIG_DEFAULT).absolute())
custom_config_file_path = str(path.joinpath(PATH_CONFIG_USER).absolute())
try:
config.read([default_config_file_path, custom_config_file_path])
# Ensure the [DEFAULT] section exists and is uppercase
if "DEFAULT" not in config:
logging.error(f'The custom config file "{custom_config_file_path}" is missing the [DEFAULT] section.')
logging.error("Make sure the first line of the file is exactly (case-sensitive): [DEFAULT]")
sleep(60)
sys.exit(1)
except configparser.MissingSectionHeaderError as error_message:
logging.error(f'Error reading "{custom_config_file_path}"')
logging.error("Make sure the first line is exactly: [DEFAULT]")
logging.error(f"{error_message}\n")
sleep(60)
sys.exit(1)
# Map config logging levels to logging module levels
LOGGING_LEVELS = {
"ERROR": logging.ERROR,
"WARNING": logging.WARNING,
"INFO": logging.INFO,
"DEBUG": logging.DEBUG,
}
# Check if the LOGGING option is valid
if "LOGGING" not in config["DEFAULT"] or config["DEFAULT"]["LOGGING"].upper() not in LOGGING_LEVELS:
logging.warning('Invalid "LOGGING" option "%s" in config file. Using default level "INFO".' % config["DEFAULT"].get("LOGGING"))
LOGGING = logging.INFO
else:
LOGGING = LOGGING_LEVELS.get(config["DEFAULT"].get("LOGGING").upper())
# Set logging level from config file
logging.basicConfig(level=LOGGING)
# List to store config errors
# This is needed else the errors are not instantly visible
errors_in_config = []
# Check if there are any options in the custom config file that are not in the default config file
default_config = configparser.ConfigParser()
custom_config = configparser.ConfigParser()
# Ensure that option names are treated as case-sensitive
default_config.optionxform = str
custom_config.optionxform = str
# Read the default and custom config files
default_config.read(default_config_file_path)
custom_config.read(custom_config_file_path)
for section in custom_config.sections() + ["DEFAULT"]:
if section not in default_config.sections() + ["DEFAULT"]:
errors_in_config.append(f'Section "{section}" in config.ini is not valid.')
else:
for option in custom_config[section]:
if option not in default_config[section]:
errors_in_config.append(f'Option "{option}" in config.ini is not valid.')
# Free up memory
del default_config, custom_config, section
# Check if option variable was set and if yes, free it
if "option" in locals():
del option
# --------- Helper Functions ---------
def get_bool_from_config(group: str, option: str) -> bool:
"""
Get a boolean value from the config file.
:param group: Group in the config file
:param option: Option in the config file
:return: Boolean value
"""
return config[group].get(option, "False").lower() == "true"
def get_float_from_config(group: str, option: str, default_value: float = 0) -> float:
"""
Get a float value from the config file.
:param group: Group in the config file
:param option: Option in the config file
:return: Float value
"""
value = config[group].get(option, default_value)
if value == "":
return default_value
try:
return float(value)
except ValueError:
errors_in_config.append(f"Invalid value '{value}' for option '{option}' in group '{group}'.")
return default_value
def get_int_from_config(group: str, option: str, default_value: int = 0) -> int:
"""
Get an integer value from the config file.
:param group: Group in the config file
:param option: Option in the config file
:return: Integer value
"""
value = config[group].get(option, default_value)
if value == "":
return default_value
try:
return int(value)
except ValueError:
errors_in_config.append(f"Invalid value '{value}' for option '{option}' in group '{group}'.")
return default_value
def get_list_from_config(group: str, option: str, mapper: Callable[[Any], Any] = lambda v: v) -> List[Any]:
"""
Get a string with comma-separated values from the config file and return a list of values.
:param group: Group in the config file
:param option: Option in the config file
:param mapper: Function to map the values to the correct type
:return: List of values
"""
try:
lines = config[group].get(option).splitlines()
cleaned = []
for line in lines:
# Remove comments (anything after ';')
line = line.split(";", 1)[0]
# Remove whitespace
line = line.strip()
# Skip empty lines
if line:
# Remove trailing commas
cleaned += line.rstrip(",").split(",")
return [mapper(item.strip()) for item in cleaned if item.strip()]
except KeyError:
logging.error(f"Missing config option '{option}' in group '{group}'")
errors_in_config.append(f"Missing config option '{option}' in group '{group}'")
return []
except ValueError:
errors_in_config.append(f"Invalid value '{mapper}' for option '{option}' in group '{group}'.")
return []
def check_config_issue(condition: bool, message: str):
"""
Check a condition and append a message to the errors_in_config list if the condition is True.
:param condition: The condition to check
:param message: The message to append if the condition is True
"""
if condition:
errors_in_config.append(f"{message}")
# SAVE CONFIG VALUES to constants
# ----- Needed hardware settings -----
NR_OF_BATTERIES: int = get_int_from_config("DEFAULT", "NR_OF_BATTERIES")
if NR_OF_BATTERIES < 2:
errors_in_config.append("NR_OF_BATTERIES must be at least 2. Currently set to %d." % NR_OF_BATTERIES)
NR_OF_CELLS_PER_BATTERY: int = get_int_from_config("DEFAULT", "NR_OF_CELLS_PER_BATTERY")
if NR_OF_CELLS_PER_BATTERY < 2:
errors_in_config.append("NR_OF_CELLS_PER_BATTERY must be at least 2. Currently set to %d." % NR_OF_CELLS_PER_BATTERY)
NR_OF_MPPTS: int = get_int_from_config("DEFAULT", "NR_OF_MPPTS")
# ----- DBus settings -----
BATTERY_SERVICE_NAME: str = config["DEFAULT"]["BATTERY_SERVICE_NAME"]
DCLOAD_SERVICE_NAME: str = config["DEFAULT"]["DCLOAD_SERVICE_NAME"]
BATTERY_PRODUCT_NAME_PATH: str = config["DEFAULT"]["BATTERY_PRODUCT_NAME_PATH"]
BATTERY_PRODUCT_NAME: str = config["DEFAULT"]["BATTERY_PRODUCT_NAME"]
BATTERY_INSTANCE_NAME_PATH: str = config["DEFAULT"]["BATTERY_INSTANCE_NAME_PATH"]
MULTI_KEYWORD: str = config["DEFAULT"]["MULTI_KEYWORD"]
MPPT_KEYWORD: str = config["DEFAULT"]["MPPT_KEYWORD"]
SMARTSHUNT_NAME_KEYWORD: str = config["DEFAULT"]["SMARTSHUNT_NAME_KEYWORD"]
SMARTSHUNT_INSTANCE_NAME_PATH: str = config["DEFAULT"]["SMARTSHUNT_INSTANCE_NAME_PATH"]
SEARCH_TRIALS: int = get_int_from_config("DEFAULT", "SEARCH_TRIALS")
READ_TRIALS: int = get_int_from_config("DEFAULT", "READ_TRIALS")
UPDATE_INTERVAL_FIND_DEVICES: int = get_int_from_config("DEFAULT", "UPDATE_INTERVAL_FIND_DEVICES")
UPDATE_INTERVAL_DATA: int = get_int_from_config("DEFAULT", "UPDATE_INTERVAL_DATA")
TIME_BEFORE_RESTART: int = get_int_from_config("DEFAULT", "TIME_BEFORE_RESTART")
# ----- Options -----
CURRENT_FROM_VICTRON: bool = get_bool_from_config("DEFAULT", "CURRENT_FROM_VICTRON")
USE_SMARTSHUNTS: bool = get_bool_from_config("DEFAULT", "USE_SMARTSHUNTS")
INVERT_SMARTSHUNTS: bool = get_bool_from_config("DEFAULT", "INVERT_SMARTSHUNTS")
IGNORE_SMARTSHUNT_ABSENCE: bool = get_bool_from_config("DEFAULT", "IGNORE_SMARTSHUNT_ABSENCE")
OWN_SOC: bool = get_bool_from_config("DEFAULT", "OWN_SOC")
ZERO_SOC: bool = get_bool_from_config("DEFAULT", "ZERO_SOC")
MAX_CELL_VOLTAGE_SOC_FULL: float = get_float_from_config("DEFAULT", "MAX_CELL_VOLTAGE_SOC_FULL")
MIN_CELL_VOLTAGE_SOC_EMPTY: float = get_float_from_config("DEFAULT", "MIN_CELL_VOLTAGE_SOC_EMPTY")
CHARGE_SAVE_PRECISION: float = get_float_from_config("DEFAULT", "CHARGE_SAVE_PRECISION")
# ----- Charge/Discharge parameters -----
OWN_CHARGE_PARAMETERS: bool = get_bool_from_config("DEFAULT", "OWN_CHARGE_PARAMETERS")
BALANCING_VOLTAGE: float = get_float_from_config("DEFAULT", "BALANCING_VOLTAGE")
BALANCING_REPETITION: int = get_int_from_config("DEFAULT", "BALANCING_REPETITION")
CHARGE_VOLTAGE_LIST: List[float] = get_list_from_config("DEFAULT", "CHARGE_VOLTAGE_LIST", float)
MAX_CELL_VOLTAGE: float = get_float_from_config("DEFAULT", "MAX_CELL_VOLTAGE")
MIN_CELL_VOLTAGE: float = get_float_from_config("DEFAULT", "MIN_CELL_VOLTAGE")
MIN_CELL_HYSTERESIS: float = get_float_from_config("DEFAULT", "MIN_CELL_HYSTERESIS")
CELL_DIFF_MAX: float = get_float_from_config("DEFAULT", "CELL_DIFF_MAX")
BATTERY_EFFICIENCY: float = get_float_from_config("DEFAULT", "BATTERY_EFFICIENCY")
MAX_CHARGE_CURRENT: int = get_int_from_config("DEFAULT", "MAX_CHARGE_CURRENT")
MAX_DISCHARGE_CURRENT: int = get_int_from_config("DEFAULT", "MAX_DISCHARGE_CURRENT")
CELL_CHARGE_LIMITING_VOLTAGE: List[float] = get_list_from_config("DEFAULT", "CELL_CHARGE_LIMITING_VOLTAGE", float)
CELL_CHARGE_LIMITED_CURRENT: List[float] = get_list_from_config("DEFAULT", "CELL_CHARGE_LIMITED_CURRENT", float)
if not CELL_CHARGE_LIMITING_VOLTAGE or len(CELL_CHARGE_LIMITING_VOLTAGE) < 2 or len(CELL_CHARGE_LIMITING_VOLTAGE) != len(CELL_CHARGE_LIMITED_CURRENT):
if not len(CELL_CHARGE_LIMITING_VOLTAGE) == 0:
errors_in_config.append("CELL_CHARGE_LIMITING_VOLTAGE is not set or has less than 2 values. Using default values.")
CELL_CHARGE_LIMITING_VOLTAGE = [
MIN_CELL_VOLTAGE,
MIN_CELL_VOLTAGE + 0.05,
BALANCING_VOLTAGE - 0.1,
BALANCING_VOLTAGE,
MAX_CELL_VOLTAGE,
]
if not CELL_CHARGE_LIMITED_CURRENT or len(CELL_CHARGE_LIMITED_CURRENT) < 2 or len(CELL_CHARGE_LIMITING_VOLTAGE) != len(CELL_CHARGE_LIMITED_CURRENT):
if not len(CELL_CHARGE_LIMITED_CURRENT) == 0:
errors_in_config.append("CELL_CHARGE_LIMITED_CURRENT is not set or has less than 2 values. Using default values.")
CELL_CHARGE_LIMITED_CURRENT = [0.2, 1, 1, 0.1, 0]
CELL_DISCHARGE_LIMITING_VOLTAGE: List[float] = get_list_from_config("DEFAULT", "CELL_DISCHARGE_LIMITING_VOLTAGE", float)
CELL_DISCHARGE_LIMITED_CURRENT: List[float] = get_list_from_config("DEFAULT", "CELL_DISCHARGE_LIMITED_CURRENT", float)
if (
not CELL_DISCHARGE_LIMITING_VOLTAGE
or len(CELL_DISCHARGE_LIMITING_VOLTAGE) < 2
or len(CELL_DISCHARGE_LIMITING_VOLTAGE) != len(CELL_DISCHARGE_LIMITED_CURRENT)
):
if not len(CELL_DISCHARGE_LIMITING_VOLTAGE) == 0:
errors_in_config.append("CELL_DISCHARGE_LIMITING_VOLTAGE is not set or has less than 2 values. Using default values.")
CELL_DISCHARGE_LIMITING_VOLTAGE = [
MIN_CELL_VOLTAGE,
MIN_CELL_VOLTAGE + 0.1,
MIN_CELL_VOLTAGE + 0.2,
]
if not CELL_DISCHARGE_LIMITED_CURRENT or len(CELL_DISCHARGE_LIMITED_CURRENT) < 2 or len(CELL_DISCHARGE_LIMITING_VOLTAGE) != len(CELL_DISCHARGE_LIMITED_CURRENT):
if not len(CELL_DISCHARGE_LIMITED_CURRENT) == 0:
errors_in_config.append("CELL_DISCHARGE_LIMITED_CURRENT is not set or has less than 2 values. Using default values.")
CELL_DISCHARGE_LIMITED_CURRENT = [0, 0.05, 1]
# --------- if OWN_CHARGE_PARAMETERS = False ---------
KEEP_MAX_CVL: bool = get_bool_from_config("DEFAULT", "KEEP_MAX_CVL")
SEND_CELL_VOLTAGES: int = get_int_from_config("DEFAULT", "SEND_CELL_VOLTAGES")
LOG_PERIOD: int = get_int_from_config("DEFAULT", "LOG_PERIOD")
# print errors and exit if there are any
if errors_in_config:
logging.error("Errors in config file:")
for error in errors_in_config:
logging.error(f"|- {error}")
logging.error("")
logging.error("Please fix the errors in the config file and restart the program.")
sleep(60)
sys.exit(1)