diff --git a/app/src/config.py b/app/src/cnf/config.py similarity index 81% rename from app/src/config.py rename to app/src/cnf/config.py index 3424bd9..ecbad0a 100644 --- a/app/src/config.py +++ b/app/src/cnf/config.py @@ -1,19 +1,23 @@ '''Config module handles the proxy configuration in the config.toml file''' -import shutil import tomllib import logging +from abc import ABC, abstractmethod from schema import Schema, And, Or, Use, Optional +class ConfigIfc(ABC): + @abstractmethod + def get_config(cls) -> dict: # pragma: no cover + pass + + class Config(): '''Static class Config is reads and sanitize the config. Read config.toml file and sanitize it with read(). Get named parts of the config with get()''' - act_config = {} - def_config = {} conf_schema = Schema({ 'tsun': { 'enabled': Use(bool), @@ -93,38 +97,14 @@ class Config(): ) @classmethod - def class_init(cls) -> None | str: # pragma: no cover - try: - # make the default config transparaent by copying it - # in the config.example file - logging.debug('Copy Default Config to config.example.toml') - - shutil.copy2("default_config.toml", - "config/config.example.toml") - except Exception: - pass - err_str = cls.read() - del cls.conf_schema - return err_str + def init(cls, ifc: ConfigIfc, path='') -> None | str: + cls.ifc = ifc + cls.act_config = {} + cls.def_config = {} + return cls.read(path) @classmethod - def _read_config_file(cls) -> dict: # pragma: no cover - usr_config = {} - - try: - with open("config/config.toml", "rb") as f: - usr_config = tomllib.load(f) - except Exception as error: - err = f'Config.read: {error}' - logging.error(err) - logging.info( - '\n To create the missing config.toml file, ' - 'you can rename the template config.example.toml\n' - ' and customize it for your scenario.\n') - return usr_config - - @classmethod - def read(cls, path='') -> None | str: + def read(cls, path) -> None | str: '''Read config file, merge it with the default config and sanitize the result''' err = None @@ -140,7 +120,7 @@ class Config(): # overwrite the default values, with values from # the config.toml file - usr_config = cls._read_config_file() + usr_config = cls.ifc.get_config() # merge the default and the user config config = def_config.copy() diff --git a/app/src/cnf/config_ifc_proxy.py b/app/src/cnf/config_ifc_proxy.py new file mode 100644 index 0000000..cf2f022 --- /dev/null +++ b/app/src/cnf/config_ifc_proxy.py @@ -0,0 +1,34 @@ +'''Config module handles the proxy configuration in the config.toml file''' + +import shutil +import tomllib +import logging +from cnf.config import ConfigIfc + + +class ConfigIfcProxy(ConfigIfc): + def __init__(self): # pragma: no cover + try: + # make the default config transparaent by copying it + # in the config.example file + logging.info('Copy Default Config to config.example.toml') + + shutil.copy2("default_config.toml", + "config/config.example.toml") + except Exception: + pass + + def get_config(self, cnf_file="config/config.toml") -> dict: + usr_config = {} + + try: + with open(cnf_file, "rb") as f: + usr_config = tomllib.load(f) + except Exception as error: + err = f'Config.read: {error}' + logging.error(err) + logging.info( + '\n To create the missing config.toml file, ' + 'you can rename the template config.example.toml\n' + ' and customize it for your scenario.\n') + return usr_config diff --git a/app/src/gen3/talent.py b/app/src/gen3/talent.py index da3ebc8..efb080a 100644 --- a/app/src/gen3/talent.py +++ b/app/src/gen3/talent.py @@ -7,7 +7,7 @@ from tzlocal import get_localzone from async_ifc import AsyncIfc from messages import Message, State from modbus import Modbus -from config import Config +from cnf.config import Config from gen3.infos_g3 import InfosG3 from infos import Register diff --git a/app/src/gen3plus/solarman_v5.py b/app/src/gen3plus/solarman_v5.py index f95894e..463c043 100644 --- a/app/src/gen3plus/solarman_v5.py +++ b/app/src/gen3plus/solarman_v5.py @@ -6,7 +6,7 @@ from datetime import datetime from async_ifc import AsyncIfc from messages import hex_dump_memory, Message, State -from config import Config +from cnf.config import Config from modbus import Modbus from gen3plus.infos_g3p import InfosG3P from infos import Register, Fmt diff --git a/app/src/inverter_base.py b/app/src/inverter_base.py index 757b883..580490c 100644 --- a/app/src/inverter_base.py +++ b/app/src/inverter_base.py @@ -12,7 +12,7 @@ from proxy import Proxy from async_stream import StreamPtr from async_stream import AsyncStreamClient from async_stream import AsyncStreamServer -from config import Config +from cnf.config import Config from infos import Infos logger_mqtt = logging.getLogger('mqtt') diff --git a/app/src/modbus_tcp.py b/app/src/modbus_tcp.py index f3788d4..7d371c9 100644 --- a/app/src/modbus_tcp.py +++ b/app/src/modbus_tcp.py @@ -2,7 +2,7 @@ import logging import traceback import asyncio -from config import Config +from cnf.config import Config from gen3plus.inverter_g3p import InverterG3P from infos import Infos diff --git a/app/src/mqtt.py b/app/src/mqtt.py index f52b797..0d33cac 100644 --- a/app/src/mqtt.py +++ b/app/src/mqtt.py @@ -5,7 +5,7 @@ import traceback from modbus import Modbus from messages import Message -from config import Config +from cnf.config import Config from singleton import Singleton logger_mqtt = logging.getLogger('mqtt') diff --git a/app/src/proxy.py b/app/src/proxy.py index eadc3ac..3f4f263 100644 --- a/app/src/proxy.py +++ b/app/src/proxy.py @@ -2,7 +2,7 @@ import asyncio import logging import json -from config import Config +from cnf.config import Config from mqtt import Mqtt from infos import Infos diff --git a/app/src/server.py b/app/src/server.py index cda8501..3f997bd 100644 --- a/app/src/server.py +++ b/app/src/server.py @@ -10,7 +10,8 @@ from inverter_ifc import InverterIfc from gen3.inverter_g3 import InverterG3 from gen3plus.inverter_g3p import InverterG3P from scheduler import Schedule -from config import Config +from cnf.config import Config +from cnf.config_ifc_proxy import ConfigIfcProxy from modbus_tcp import ModbusTcp routes = web.RouteTableDef() @@ -149,7 +150,7 @@ if __name__ == "__main__": asyncio.set_event_loop(loop) # read config file - ConfigErr = Config.class_init() + ConfigErr = Config.init(ConfigIfcProxy()) if ConfigErr is not None: logging.info(f'ConfigErr: {ConfigErr}') Proxy.class_init() diff --git a/app/tests/test_config.py b/app/tests/test_config.py index aaac45c..3eb2169 100644 --- a/app/tests/test_config.py +++ b/app/tests/test_config.py @@ -1,16 +1,16 @@ # test_with_pytest.py import tomllib from schema import SchemaMissingKeyError -from config import Config +from cnf.config import Config, ConfigIfc -class TstConfig(Config): +class TstConfig(ConfigIfc): @classmethod - def set(cls, cnf): + def __init__(cls, cnf): cls.act_config = cnf @classmethod - def _read_config_file(cls) -> dict: + def get_config(cls) -> dict: return cls.act_config @@ -93,10 +93,9 @@ def test_mininum_config(): def test_read_empty(): cnf = {} - TstConfig.set(cnf) - err = TstConfig.read('app/config/') + err = Config.init(TstConfig(cnf), 'app/config/') assert err == None - cnf = TstConfig.get() + cnf = Config.get() assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': { 'allow_all': False, @@ -129,26 +128,24 @@ def test_read_empty(): } } - defcnf = TstConfig.def_config.get('solarman') + defcnf = Config.def_config.get('solarman') assert defcnf == {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000} - assert True == TstConfig.is_default('solarman') + assert True == Config.is_default('solarman') def test_no_file(): cnf = {} - TstConfig.set(cnf) - err = TstConfig.read('') + err = Config.init(TstConfig(cnf), '') assert err == "Config.read: [Errno 2] No such file or directory: 'default_config.toml'" - cnf = TstConfig.get() + cnf = Config.get() assert cnf == {} - defcnf = TstConfig.def_config.get('solarman') + defcnf = Config.def_config.get('solarman') assert defcnf == None def test_read_cnf1(): cnf = {'solarman' : {'enabled': False}} - TstConfig.set(cnf) - err = TstConfig.read('app/config/') + err = Config.init(TstConfig(cnf), 'app/config/') assert err == None - cnf = TstConfig.get() + cnf = Config.get() assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': False, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': { 'allow_all': False, @@ -180,18 +177,17 @@ def test_read_cnf1(): } } } - cnf = TstConfig.get('solarman') + cnf = Config.get('solarman') assert cnf == {'enabled': False, 'host': 'iot.talent-monitoring.com', 'port': 10000} - defcnf = TstConfig.def_config.get('solarman') + defcnf = Config.def_config.get('solarman') assert defcnf == {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000} - assert False == TstConfig.is_default('solarman') + assert False == Config.is_default('solarman') def test_read_cnf2(): cnf = {'solarman' : {'enabled': 'FALSE'}} - TstConfig.set(cnf) - err = TstConfig.read('app/config/') + err = Config.init(TstConfig(cnf), 'app/config/') assert err == None - cnf = TstConfig.get() + cnf = Config.get() assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': { 'allow_all': False, @@ -223,22 +219,20 @@ def test_read_cnf2(): } } } - assert True == TstConfig.is_default('solarman') + assert True == Config.is_default('solarman') def test_read_cnf3(): cnf = {'solarman' : {'port': 'FALSE'}} - TstConfig.set(cnf) - err = TstConfig.read('app/config/') + err = Config.init(TstConfig(cnf), 'app/config/') assert err == 'Config.read: Key \'solarman\' error:\nKey \'port\' error:\nint(\'FALSE\') raised ValueError("invalid literal for int() with base 10: \'FALSE\'")' - cnf = TstConfig.get() - assert cnf == {'solarman': {'port': 'FALSE'}} + cnf = Config.get() + assert cnf == {} def test_read_cnf4(): cnf = {'solarman' : {'port': 5000}} - TstConfig.set(cnf) - err = TstConfig.read('app/config/') + err = Config.init(TstConfig(cnf), 'app/config/') assert err == None - cnf = TstConfig.get() + cnf = Config.get() assert cnf == {'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 5000}, 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}, 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, 'inverters': { 'allow_all': False, @@ -270,16 +264,14 @@ def test_read_cnf4(): } } } - assert False == TstConfig.is_default('solarman') + assert False == Config.is_default('solarman') def test_read_cnf5(): cnf = {'solarman' : {'port': 1023}} - TstConfig.set(cnf) - err = TstConfig.read('app/config/') + err = Config.init(TstConfig(cnf), 'app/config/') assert err != None def test_read_cnf6(): cnf = {'solarman' : {'port': 65536}} - TstConfig.set(cnf) - err = TstConfig.read('app/config/') + err = Config.init(TstConfig(cnf), 'app/config/') assert err != None diff --git a/app/tests/test_config_ifc_proxy.py b/app/tests/test_config_ifc_proxy.py new file mode 100644 index 0000000..02b0ec7 --- /dev/null +++ b/app/tests/test_config_ifc_proxy.py @@ -0,0 +1,53 @@ +# test_with_pytest.py +import tomllib +from schema import SchemaMissingKeyError +from cnf.config_ifc_proxy import ConfigIfcProxy + +class CnfIfc(ConfigIfcProxy): + def __init__(self): + pass + +def test_no_config(): + cnf_ifc = CnfIfc() + + cnf = cnf_ifc.get_config("") + assert cnf == {} + +def test_get_config(): + cnf_ifc = CnfIfc() + + cnf = cnf_ifc.get_config("app/config/default_config.toml") + assert cnf == { + 'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': []}, 'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'], 'block': []}}}, + 'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005}, + 'solarman': {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}, + 'mqtt': {'host': 'mqtt', 'port': 1883, 'user': '', 'passwd': ''}, + 'ha': {'auto_conf_prefix': 'homeassistant', 'discovery_prefix': 'homeassistant', 'entity_prefix': 'tsun', 'proxy_node_id': 'proxy', 'proxy_unique_id': 'P170000000000001'}, + 'inverters': { + 'allow_all': False, + 'R170000000000001': { + 'node_id': '', + 'pv1': {'manufacturer': 'Risen', + 'type': 'RSM40-8-395M'}, + 'pv2': {'manufacturer': 'Risen', + 'type': 'RSM40-8-395M'}, + 'modbus_polling': False, + 'suggested_area': '' + }, + 'Y170000000000001': { + 'modbus_polling': True, + 'monitor_sn': 2000000000, + 'node_id': '', + 'pv1': {'manufacturer': 'Risen', + 'type': 'RSM40-8-410M'}, + 'pv2': {'manufacturer': 'Risen', + 'type': 'RSM40-8-410M'}, + 'pv3': {'manufacturer': 'Risen', + 'type': 'RSM40-8-410M'}, + 'pv4': {'manufacturer': 'Risen', + 'type': 'RSM40-8-410M'}, + 'suggested_area': '' + } + } + } + diff --git a/app/tests/test_inverter_base.py b/app/tests/test_inverter_base.py index 5962a49..2e05777 100644 --- a/app/tests/test_inverter_base.py +++ b/app/tests/test_inverter_base.py @@ -6,7 +6,7 @@ import gc from mock import patch from enum import Enum from infos import Infos -from config import Config +from cnf.config import Config from gen3.talent import Talent from inverter_base import InverterBase from singleton import Singleton diff --git a/app/tests/test_inverter_g3.py b/app/tests/test_inverter_g3.py index a841dbc..620173c 100644 --- a/app/tests/test_inverter_g3.py +++ b/app/tests/test_inverter_g3.py @@ -6,7 +6,7 @@ import sys,gc from mock import patch from enum import Enum from infos import Infos -from config import Config +from cnf.config import Config from proxy import Proxy from inverter_base import InverterBase from singleton import Singleton diff --git a/app/tests/test_inverter_g3p.py b/app/tests/test_inverter_g3p.py index 307018b..6bb98ed 100644 --- a/app/tests/test_inverter_g3p.py +++ b/app/tests/test_inverter_g3p.py @@ -5,7 +5,7 @@ import asyncio from mock import patch from enum import Enum from infos import Infos -from config import Config +from cnf.config import Config from proxy import Proxy from inverter_base import InverterBase from singleton import Singleton diff --git a/app/tests/test_modbus_tcp.py b/app/tests/test_modbus_tcp.py index e901e96..1c69b60 100644 --- a/app/tests/test_modbus_tcp.py +++ b/app/tests/test_modbus_tcp.py @@ -6,7 +6,7 @@ from aiomqtt import MqttCodeError from mock import patch from enum import Enum from singleton import Singleton -from config import Config +from cnf.config import Config from infos import Infos from mqtt import Mqtt from inverter_base import InverterBase diff --git a/app/tests/test_mqtt.py b/app/tests/test_mqtt.py index 91acd02..9c2923c 100644 --- a/app/tests/test_mqtt.py +++ b/app/tests/test_mqtt.py @@ -10,7 +10,7 @@ from singleton import Singleton from mqtt import Mqtt from modbus import Modbus from gen3plus.solarman_v5 import SolarmanV5 -from config import Config +from cnf.config import Config pytest_plugins = ('pytest_asyncio',) diff --git a/app/tests/test_proxy.py b/app/tests/test_proxy.py index aa6c739..ace707e 100644 --- a/app/tests/test_proxy.py +++ b/app/tests/test_proxy.py @@ -9,7 +9,7 @@ from singleton import Singleton from proxy import Proxy from mqtt import Mqtt from gen3plus.solarman_v5 import SolarmanV5 -from config import Config +from cnf.config import Config pytest_plugins = ('pytest_asyncio',) diff --git a/app/tests/test_solarman.py b/app/tests/test_solarman.py index 6d19688..6f11bec 100644 --- a/app/tests/test_solarman.py +++ b/app/tests/test_solarman.py @@ -7,7 +7,7 @@ import random from math import isclose from async_stream import AsyncIfcImpl, StreamPtr from gen3plus.solarman_v5 import SolarmanV5, SolarmanBase -from config import Config +from cnf.config import Config from infos import Infos, Register from modbus import Modbus from messages import State, Message diff --git a/app/tests/test_talent.py b/app/tests/test_talent.py index 32fd6fe..2b1ef6c 100644 --- a/app/tests/test_talent.py +++ b/app/tests/test_talent.py @@ -3,7 +3,7 @@ import pytest, logging, asyncio from math import isclose from async_stream import AsyncIfcImpl, StreamPtr from gen3.talent import Talent, Control -from config import Config +from cnf.config import Config from infos import Infos, Register from modbus import Modbus from messages import State