From 0591d5c4768738f3c1f7ba778d56ab5a1a4d2510 Mon Sep 17 00:00:00 2001 From: Ben Pirt Date: Thu, 21 Dec 2023 08:47:13 +0000 Subject: [PATCH] Make the persistent config more modular and simpler --- .gitignore | 4 +- config.default.json | 36 ---- config.py | 59 +------ lib/__init__.py | 1 + lib/persistent_config.py | 48 ++++++ platforms/cpython/pyBms.py | 2 +- scripts/data_to_py.py | 154 ------------------ .../test_TeslaModelSBatteryModule.py | 4 +- .../test_TeslaModelSBatteryPack.py | 4 +- .../test_TeslaModelSNetworkGateway.py | 2 +- test/battery/test_BatteryCell.py | 2 +- test/battery/test_BatteryModule.py | 2 +- test/battery/test_BatteryPack.py | 2 +- test/battery/test_BatteryString.py | 2 +- test/bms/test_Bms.py | 2 +- test/bms/test_Config.py | 64 -------- test/bms/test_ContactorControl.py | 2 +- test/lib/__init__.py | 0 test/lib/test_PersistentConfig.py | 61 +++++++ 19 files changed, 131 insertions(+), 320 deletions(-) delete mode 100644 config.default.json create mode 100644 lib/__init__.py create mode 100644 lib/persistent_config.py delete mode 100755 scripts/data_to_py.py delete mode 100644 test/bms/test_Config.py create mode 100644 test/lib/__init__.py create mode 100644 test/lib/test_PersistentConfig.py diff --git a/.gitignore b/.gitignore index 90598f6..db23789 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,6 @@ dist/ downloads/ eggs/ .eggs/ -lib/ lib64/ parts/ sdist/ @@ -130,6 +129,5 @@ dmypy.json .picowgo .vscode build/out -config.json -config_json.py +initial_config.py ui/node_modules diff --git a/config.default.json b/config.default.json deleted file mode 100644 index 7748f54..0000000 --- a/config.default.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "module_count": 2, - "parallel_string_count": 1, - "cell_high_voltage_setpoint": 4.1, - "cell_low_voltage_setpoint": 3.6, - "voltage_alert_offset": 0.025, - "voltage_fault_offset": 0.05, - "high_temperature_setpoint": 65.0, - "low_temperature_setpoint": 10.0, - "comms_timeout": 10.0, - "negative_pin": 16, - "precharge_pin": 17, - "positive_pin": 26, - "balancing_enabled": true, - "balance_time": 5.0, - "balance_voltage": 3.9, - "balance_difference": 0.04, - "module_capacity": 232.0, - "max_cell_voltage_difference": 0.2, - "temperature_warning_offset": 5.0, - "poll_interval": 0.5, - "led_pin": 18, - "soc_lookup": [ - [3.0, 0.0], - [3.1, 0.1], - [4.1, 0.9], - [4.2, 1.0] - ], - "wifi_network": "", - "wifi_password": "", - "hardware_fault_detection": false, - "web_server_port": 80, - "current_zero_point": 1.6025, - "current_reversed": false, - "current_sensor_max": 200 -} diff --git a/config.py b/config.py index 501edc5..8561414 100644 --- a/config.py +++ b/config.py @@ -1,18 +1,12 @@ -import os -import json +from lib import PersistentConfig +try: + from initial_config import config # type: ignore +except ImportError: + config = {} -def exists(filename: str) -> bool: - try: - os.stat(filename) - return True - except OSError: - return False - - -class Config: - def __init__(self, file="config.json"): - self.__file = file +class Config (PersistentConfig): + def __init__(self): # Initialise the defaults # The number of modules in the pack @@ -127,7 +121,7 @@ def __init__(self, file="config.json"): # How long between sending a full update self.mqtt_full_output_interval: float = 120.0 - self.read() + super().__init__(config) @property def high_voltage_alert_level(self): @@ -144,40 +138,3 @@ def low_voltage_alert_level(self): @property def low_voltage_fault_level(self): return self.cell_low_voltage_setpoint - self.voltage_fault_offset - - def get_dict(self): - return {k: v for k, v in self.__dict__.items() if not k.startswith("_")} - - def read(self): - data = None - if exists(self.__file): - with open(self.__file, 'r', encoding="UTF-8") as file: - data = file.read() - else: - try: - # pylint: disable=E0401 - import config_json # type: ignore - data = bytearray(config_json.data()).decode() - except ModuleNotFoundError: - print("Error reading default python config") - - if data: - new_config = json.loads(data) - self.update(new_config) - - def set_value(self, key: str, value): - if hasattr(self, key): - if type(getattr(self, key)) == type(value) or type(value) == type(None): - setattr(self, key, value) - else: - print(f"Config types did not match: {key} ({type(getattr(self, key))}) ({type(value)})") - else: - print("Attribute does not exist") - - def update(self, new_config: dict) -> None: - for (key, value) in new_config.items(): - self.set_value(key, value) - - def save(self): - with open(self.__file, 'w', encoding="utf-8") as file: - json.dump(self.get_dict(), file) diff --git a/lib/__init__.py b/lib/__init__.py new file mode 100644 index 0000000..24fe776 --- /dev/null +++ b/lib/__init__.py @@ -0,0 +1 @@ +from .persistent_config import PersistentConfig as PersistentConfig diff --git a/lib/persistent_config.py b/lib/persistent_config.py new file mode 100644 index 0000000..9859087 --- /dev/null +++ b/lib/persistent_config.py @@ -0,0 +1,48 @@ +import os +import json + + +def exists(filename: str) -> bool: + try: + os.stat(filename) + return True + except OSError: + return False + + +class PersistentConfig: + def __init__(self, initial_config={}, file="config.json"): + self.__file = file + + self.update(initial_config) + self.read() + + def get_dict(self): + return {k: v for k, v in self.__dict__.items() if not k.startswith("_")} + + def read(self): + data = None + if exists(self.__file): + with open(self.__file, 'r', encoding="UTF-8") as file: + data = file.read() + if data: + new_config = json.loads(data) + self.update(new_config) + + def set_value(self, key: str, value): + if hasattr(self, key): + if type(getattr(self, key)) == type(value) or type(value) == type(None): + setattr(self, key, value) + else: + print( + f"Config types did not match: {key} ({type(getattr(self, key))}) ({type(value)})") + else: + print("Attribute does not exist") + + def update(self, new_config: dict) -> None: + for (key, value) in new_config.items(): + self.set_value(key, value) + + def save(self): + with open(self.__file, 'w', encoding="utf-8") as file: + json.dump(self.get_dict(), file) diff --git a/platforms/cpython/pyBms.py b/platforms/cpython/pyBms.py index 31a7605..757ad94 100644 --- a/platforms/cpython/pyBms.py +++ b/platforms/cpython/pyBms.py @@ -14,7 +14,7 @@ baud = int(sys.argv[2]) serialPort = serial.Serial(port, baud, timeout=0.1) -config = Config("config.local.json") +config = Config() gateway = TeslaModelSNetworkGateway(serialPort, config) pack = TeslaModelSBatteryPack(gateway, config) bms = Bms(pack, config) diff --git a/scripts/data_to_py.py b/scripts/data_to_py.py deleted file mode 100755 index 6c951f9..0000000 --- a/scripts/data_to_py.py +++ /dev/null @@ -1,154 +0,0 @@ -#! /usr/bin/python3 -# -*- coding: utf-8 -*- - -# The MIT License (MIT) -# -# Copyright (c) 2016 Peter Hinch -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -import argparse -import sys -import os - -# UTILITIES FOR WRITING PYTHON SOURCECODE TO A FILE - -# ByteWriter takes as input a variable name and data values and writes -# Python source to an output stream of the form -# my_variable = b'\x01\x02\x03\x04\x05\x06\x07\x08'\ - -# Lines are broken with \ for readability. - - -class ByteWriter(object): - bytes_per_line = 16 - - def __init__(self, stream, varname): - self.stream = stream - self.stream.write('{} =\\\n'.format(varname)) - self.bytecount = 0 # For line breaks - - def _eol(self): - self.stream.write("'\\\n") - - def _eot(self): - self.stream.write("'\n") - - def _bol(self): - self.stream.write("b'") - - # Output a single byte - def obyte(self, data): - if not self.bytecount: - self._bol() - self.stream.write('\\x{:02x}'.format(data)) - self.bytecount += 1 - self.bytecount %= self.bytes_per_line - if not self.bytecount: - self._eol() - - # Output from a sequence - def odata(self, bytelist): - for byt in bytelist: - self.obyte(byt) - - # ensure a correct final line - def eot(self): # User force EOL if one hasn't occurred - if self.bytecount: - self._eot() - self.stream.write('\n') - - -# PYTHON FILE WRITING - -STR01 = """# Code generated by data_to_py.py. -version = '0.1' -""" - -STR02 = """_mvdata = memoryview(_data) - -def data(): - return _mvdata - -""" - - -def write_func(stream, name, arg): - stream.write('def {}():\n return {}\n\n'.format(name, arg)) - - -def write_data(op_path, ip_path): - try: - with open(ip_path, 'rb') as ip_stream: - try: - with open(op_path, 'w') as op_stream: - write_stream(ip_stream, op_stream) - except OSError: - print("Can't open", op_path, 'for writing') - return False - except OSError: - print("Can't open", ip_path) - return False - return True - - -def write_stream(ip_stream, op_stream): - op_stream.write(STR01) - op_stream.write('\n') - data = ip_stream.read() - bw_data = ByteWriter(op_stream, '_data') - bw_data.odata(data) - bw_data.eot() - op_stream.write(STR02) - - -# PARSE COMMAND LINE ARGUMENTS - -def quit(msg): - print(msg) - sys.exit(1) - - -DESC = """data_to_py.py -Utility to convert an arbitrary binary file to Python source. -Sample usage: -data_to_py.py image.jpg image.py - -""" - -if __name__ == "__main__": - parser = argparse.ArgumentParser(__file__, description=DESC, - formatter_class=argparse.RawDescriptionHelpFormatter) - parser.add_argument('infile', type=str, help='Input file path') - parser.add_argument('outfile', type=str, - help='Path and name of output file. Must have .py extension.') - - args = parser.parse_args() - - if not os.path.isfile(args.infile): - quit("Data filename does not exist") - - if not os.path.splitext(args.outfile)[1].upper() == '.PY': - quit('Output filename must have a .py extension.') - - print('Writing Python file.') - if not write_data(args.outfile, args.infile): - sys.exit(1) - - print(args.outfile, 'written successfully.') diff --git a/test/battery/tesla_model_s/test_TeslaModelSBatteryModule.py b/test/battery/tesla_model_s/test_TeslaModelSBatteryModule.py index c74f7d0..7068593 100644 --- a/test/battery/tesla_model_s/test_TeslaModelSBatteryModule.py +++ b/test/battery/tesla_model_s/test_TeslaModelSBatteryModule.py @@ -32,8 +32,8 @@ def write_register(self, address: int, register: int, value: int, attempts: int class TeslaModelSBatteryModuleTestCase(unittest.TestCase): def setUp(self): - c = Config("config.default.json") - self.mockGateway = FakeGateway(None, Config("config.default.json")) + c = Config() + self.mockGateway = FakeGateway(None, Config()) self.mockGateway.write_register = MagicMock(return_value=True) self.module = TeslaModelSBatteryModule(1, self.mockGateway, c) return super().setUp() diff --git a/test/battery/tesla_model_s/test_TeslaModelSBatteryPack.py b/test/battery/tesla_model_s/test_TeslaModelSBatteryPack.py index 1d3c296..7f1e24a 100644 --- a/test/battery/tesla_model_s/test_TeslaModelSBatteryPack.py +++ b/test/battery/tesla_model_s/test_TeslaModelSBatteryPack.py @@ -27,9 +27,9 @@ def write_register(self, address: int, register: int, value: int, attempts: int class TeslaModelSBatteryPackTestCase(unittest.TestCase): def setUp(self): - self.mockGateway = FakeGateway(None, Config("config.default.json")) + self.mockGateway = FakeGateway(None, Config()) self.mockGateway.write_register = MagicMock(return_value=True) - self.config = Config("config.default.json") + self.config = Config() self.config.module_count = 2 return super().setUp() diff --git a/test/battery/tesla_model_s/test_TeslaModelSNetworkGateway.py b/test/battery/tesla_model_s/test_TeslaModelSNetworkGateway.py index 411880d..7674be2 100644 --- a/test/battery/tesla_model_s/test_TeslaModelSNetworkGateway.py +++ b/test/battery/tesla_model_s/test_TeslaModelSNetworkGateway.py @@ -19,7 +19,7 @@ def setUp(self): self.serial = FakeSerial() self.serial.write = MagicMock() self.gateway = TeslaModelSNetworkGateway( - self.serial, Config("config.default.json")) + self.serial, Config()) return super().setUp() def test_write_register(self): diff --git a/test/battery/test_BatteryCell.py b/test/battery/test_BatteryCell.py index af401b3..491ed18 100644 --- a/test/battery/test_BatteryCell.py +++ b/test/battery/test_BatteryCell.py @@ -8,7 +8,7 @@ class BatteryCellTestCase(unittest.TestCase): def setUp(self): - c = Config("config.default.json") + c = Config() c.over_voltage_hysteresis_time = 0.01 self.cell = BatteryCell(c) diff --git a/test/battery/test_BatteryModule.py b/test/battery/test_BatteryModule.py index 014364b..406dd0e 100644 --- a/test/battery/test_BatteryModule.py +++ b/test/battery/test_BatteryModule.py @@ -9,7 +9,7 @@ class BatteryModuleTestCase(unittest.TestCase): def setUp(self): - c = Config("config.default.json") + c = Config() c.over_voltage_hysteresis_time = 0.01 self.module = BatteryModule(c) for i in range(4): diff --git a/test/battery/test_BatteryPack.py b/test/battery/test_BatteryPack.py index 98e28cd..75addce 100644 --- a/test/battery/test_BatteryPack.py +++ b/test/battery/test_BatteryPack.py @@ -7,7 +7,7 @@ class BatteryPackTestCase(unittest.TestCase): def setUp(self): - self.config = Config("config.default.json") + self.config = Config() self.config.parallel_string_count = 2 self.config.over_voltage_hysteresis_time = 0.01 self.pack = BatteryPack(self.config) diff --git a/test/battery/test_BatteryString.py b/test/battery/test_BatteryString.py index da1b4eb..34370c0 100644 --- a/test/battery/test_BatteryString.py +++ b/test/battery/test_BatteryString.py @@ -8,7 +8,7 @@ class TeslaModelSBatteryModuleTestCase(unittest.TestCase): def setUp(self): - self.config = Config("config.default.json") + self.config = Config() self.config.balance_difference = 0.1 self.config.balancing_enabled = True self.config.balance_voltage = 3.6 diff --git a/test/bms/test_Bms.py b/test/bms/test_Bms.py index 1e53cc5..79e3835 100644 --- a/test/bms/test_Bms.py +++ b/test/bms/test_Bms.py @@ -26,7 +26,7 @@ def high_cell_voltage(self): class BmsTestCase(unittest.TestCase): def setUp(self): - self.config = Config("config.default.json") + self.config = Config() self.pack = FakePack() self.current_sensor = FakeCurrentSensor() self.bms = Bms(self.pack, self.config, self.current_sensor) diff --git a/test/bms/test_Config.py b/test/bms/test_Config.py deleted file mode 100644 index 24709ad..0000000 --- a/test/bms/test_Config.py +++ /dev/null @@ -1,64 +0,0 @@ -import os -import json -import unittest -from os import path # type: ignore -from config import Config -from scripts.data_to_py import write_data - - -def get_dummy_config(): - with open("config.default.json", "r") as fp: - return json.load(fp) - - -def remove_json(): - try: - os.remove("/tmp/config.json") - except Exception: - pass - - -def remove_py(): - try: - os.remove("config_json.py") - except Exception: - pass - - -class ConfigTestCase(unittest.TestCase): - def setUp(self): - remove_json() - remove_py() - return super().setUp() - - def test_load_builtins(self): - c = Config("no-file.json") - self.assertEqual(c.module_count, 2) - - def test_load_default(self): - c = get_dummy_config() - c["module_count"] = 4 - with open("/tmp/config.json", "w") as fp: - json.dump(c, fp) - write_data("config_json.py", "/tmp/config.json") - c = Config("no-file.json") - self.assertEqual(c.module_count, 4) - - def test_load_config_json(self): - c = get_dummy_config() - c["module_count"] = 8 - with open("/tmp/config.json", "w") as fp: - json.dump(c, fp) - write_data("config_json.py", "/tmp/config.json") - c = Config("/tmp/config.json") - self.assertEqual(c.module_count, 8) - - def test_save(self): - self.assertFalse(path.exists("/tmp/config.json")) - c = Config("/tmp/config.json") - c.module_count = 12 - c.save() - self.assertTrue(path.exists("/tmp/config.json")) - with open("/tmp/config.json", "r") as fp: - savedConfig = json.load(fp) - self.assertEqual(savedConfig["module_count"], 12) diff --git a/test/bms/test_ContactorControl.py b/test/bms/test_ContactorControl.py index e4d59b1..c400b6e 100644 --- a/test/bms/test_ContactorControl.py +++ b/test/bms/test_ContactorControl.py @@ -7,7 +7,7 @@ class ContactorControlTestCase(unittest.TestCase): def setUp(self): - config = Config("config.default.json") + config = Config() config.contactor_negative_time = 0.1 config.contactor_precharge_time = 0.1 self.control = ContactorControl(config) diff --git a/test/lib/__init__.py b/test/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/lib/test_PersistentConfig.py b/test/lib/test_PersistentConfig.py new file mode 100644 index 0000000..02e9090 --- /dev/null +++ b/test/lib/test_PersistentConfig.py @@ -0,0 +1,61 @@ +import os +import json +import unittest +from os import path # type: ignore +from lib import PersistentConfig + +TEST_CONFIG = "/tmp/config.json" + + +def remove_json(): + try: + os.remove(TEST_CONFIG) + except Exception: + pass + + +def remove_py(): + try: + os.remove("config_json.py") + except Exception: + pass + + +class TestConfig(PersistentConfig): + def __init__(self, defaults={}, config_file=TEST_CONFIG): + self.test_value = 1 + + super().__init__(defaults, config_file) + + +class ConfigTestCase(unittest.TestCase): + def setUp(self): + remove_json() + remove_py() + return super().setUp() + + def test_load_builtins(self): + c = TestConfig() + self.assertEqual(c.test_value, 1) + + def test_load_default(self): + conf = {"test_value": 4} + c = TestConfig(conf) + self.assertEqual(c.test_value, 4) + + def test_load_config_json(self): + conf = {"test_value": 3} + with open(TEST_CONFIG, "w") as fp: + json.dump(conf, fp) + c = TestConfig({}, TEST_CONFIG) + self.assertEqual(c.test_value, 3) + + def test_save(self): + self.assertFalse(path.exists(TEST_CONFIG)) + c = TestConfig({}, TEST_CONFIG) + c.test_value = 12 + c.save() + self.assertTrue(path.exists(TEST_CONFIG)) + with open(TEST_CONFIG, "r") as fp: + savedConfig = json.load(fp) + self.assertEqual(savedConfig["test_value"], 12)