S allius/issue217 (#229)

* move config.py into a sub directory cnf

* adapt unit test

* split config class

- use depency injection to get config

* increase test coverage
This commit is contained in:
Stefan Allius
2024-12-03 22:02:23 +01:00
committed by GitHub
parent 668c631018
commit a5b2b4b7c2
19 changed files with 145 additions and 85 deletions

View File

@@ -1,19 +1,23 @@
'''Config module handles the proxy configuration in the config.toml file''' '''Config module handles the proxy configuration in the config.toml file'''
import shutil
import tomllib import tomllib
import logging import logging
from abc import ABC, abstractmethod
from schema import Schema, And, Or, Use, Optional from schema import Schema, And, Or, Use, Optional
class ConfigIfc(ABC):
@abstractmethod
def get_config(cls) -> dict: # pragma: no cover
pass
class Config(): class Config():
'''Static class Config is reads and sanitize the config. '''Static class Config is reads and sanitize the config.
Read config.toml file and sanitize it with read(). Read config.toml file and sanitize it with read().
Get named parts of the config with get()''' Get named parts of the config with get()'''
act_config = {}
def_config = {}
conf_schema = Schema({ conf_schema = Schema({
'tsun': { 'tsun': {
'enabled': Use(bool), 'enabled': Use(bool),
@@ -93,38 +97,14 @@ class Config():
) )
@classmethod @classmethod
def class_init(cls) -> None | str: # pragma: no cover def init(cls, ifc: ConfigIfc, path='') -> None | str:
try: cls.ifc = ifc
# make the default config transparaent by copying it cls.act_config = {}
# in the config.example file cls.def_config = {}
logging.debug('Copy Default Config to config.example.toml') return cls.read(path)
shutil.copy2("default_config.toml",
"config/config.example.toml")
except Exception:
pass
err_str = cls.read()
del cls.conf_schema
return err_str
@classmethod @classmethod
def _read_config_file(cls) -> dict: # pragma: no cover def read(cls, path) -> None | str:
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:
'''Read config file, merge it with the default config '''Read config file, merge it with the default config
and sanitize the result''' and sanitize the result'''
err = None err = None
@@ -140,7 +120,7 @@ class Config():
# overwrite the default values, with values from # overwrite the default values, with values from
# the config.toml file # the config.toml file
usr_config = cls._read_config_file() usr_config = cls.ifc.get_config()
# merge the default and the user config # merge the default and the user config
config = def_config.copy() config = def_config.copy()

View File

@@ -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

View File

@@ -7,7 +7,7 @@ from tzlocal import get_localzone
from async_ifc import AsyncIfc from async_ifc import AsyncIfc
from messages import Message, State from messages import Message, State
from modbus import Modbus from modbus import Modbus
from config import Config from cnf.config import Config
from gen3.infos_g3 import InfosG3 from gen3.infos_g3 import InfosG3
from infos import Register from infos import Register

View File

@@ -6,7 +6,7 @@ from datetime import datetime
from async_ifc import AsyncIfc from async_ifc import AsyncIfc
from messages import hex_dump_memory, Message, State from messages import hex_dump_memory, Message, State
from config import Config from cnf.config import Config
from modbus import Modbus from modbus import Modbus
from gen3plus.infos_g3p import InfosG3P from gen3plus.infos_g3p import InfosG3P
from infos import Register, Fmt from infos import Register, Fmt

View File

@@ -12,7 +12,7 @@ from proxy import Proxy
from async_stream import StreamPtr from async_stream import StreamPtr
from async_stream import AsyncStreamClient from async_stream import AsyncStreamClient
from async_stream import AsyncStreamServer from async_stream import AsyncStreamServer
from config import Config from cnf.config import Config
from infos import Infos from infos import Infos
logger_mqtt = logging.getLogger('mqtt') logger_mqtt = logging.getLogger('mqtt')

View File

@@ -2,7 +2,7 @@ import logging
import traceback import traceback
import asyncio import asyncio
from config import Config from cnf.config import Config
from gen3plus.inverter_g3p import InverterG3P from gen3plus.inverter_g3p import InverterG3P
from infos import Infos from infos import Infos

View File

@@ -5,7 +5,7 @@ import traceback
from modbus import Modbus from modbus import Modbus
from messages import Message from messages import Message
from config import Config from cnf.config import Config
from singleton import Singleton from singleton import Singleton
logger_mqtt = logging.getLogger('mqtt') logger_mqtt = logging.getLogger('mqtt')

View File

@@ -2,7 +2,7 @@ import asyncio
import logging import logging
import json import json
from config import Config from cnf.config import Config
from mqtt import Mqtt from mqtt import Mqtt
from infos import Infos from infos import Infos

View File

@@ -10,7 +10,8 @@ from inverter_ifc import InverterIfc
from gen3.inverter_g3 import InverterG3 from gen3.inverter_g3 import InverterG3
from gen3plus.inverter_g3p import InverterG3P from gen3plus.inverter_g3p import InverterG3P
from scheduler import Schedule 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 from modbus_tcp import ModbusTcp
routes = web.RouteTableDef() routes = web.RouteTableDef()
@@ -149,7 +150,7 @@ if __name__ == "__main__":
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
# read config file # read config file
ConfigErr = Config.class_init() ConfigErr = Config.init(ConfigIfcProxy())
if ConfigErr is not None: if ConfigErr is not None:
logging.info(f'ConfigErr: {ConfigErr}') logging.info(f'ConfigErr: {ConfigErr}')
Proxy.class_init() Proxy.class_init()

View File

@@ -1,16 +1,16 @@
# test_with_pytest.py # test_with_pytest.py
import tomllib import tomllib
from schema import SchemaMissingKeyError from schema import SchemaMissingKeyError
from config import Config from cnf.config import Config, ConfigIfc
class TstConfig(Config): class TstConfig(ConfigIfc):
@classmethod @classmethod
def set(cls, cnf): def __init__(cls, cnf):
cls.act_config = cnf cls.act_config = cnf
@classmethod @classmethod
def _read_config_file(cls) -> dict: def get_config(cls) -> dict:
return cls.act_config return cls.act_config
@@ -93,10 +93,9 @@ def test_mininum_config():
def test_read_empty(): def test_read_empty():
cnf = {} cnf = {}
TstConfig.set(cnf) err = Config.init(TstConfig(cnf), 'app/config/')
err = TstConfig.read('app/config/')
assert err == None 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'}, 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': { 'inverters': {
'allow_all': False, '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 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(): def test_no_file():
cnf = {} cnf = {}
TstConfig.set(cnf) err = Config.init(TstConfig(cnf), '')
err = TstConfig.read('')
assert err == "Config.read: [Errno 2] No such file or directory: 'default_config.toml'" assert err == "Config.read: [Errno 2] No such file or directory: 'default_config.toml'"
cnf = TstConfig.get() cnf = Config.get()
assert cnf == {} assert cnf == {}
defcnf = TstConfig.def_config.get('solarman') defcnf = Config.def_config.get('solarman')
assert defcnf == None assert defcnf == None
def test_read_cnf1(): def test_read_cnf1():
cnf = {'solarman' : {'enabled': False}} cnf = {'solarman' : {'enabled': False}}
TstConfig.set(cnf) err = Config.init(TstConfig(cnf), 'app/config/')
err = TstConfig.read('app/config/')
assert err == None 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'}, 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': { 'inverters': {
'allow_all': False, '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} 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 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(): def test_read_cnf2():
cnf = {'solarman' : {'enabled': 'FALSE'}} cnf = {'solarman' : {'enabled': 'FALSE'}}
TstConfig.set(cnf) err = Config.init(TstConfig(cnf), 'app/config/')
err = TstConfig.read('app/config/')
assert err == None 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'}, 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': { 'inverters': {
'allow_all': False, '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(): def test_read_cnf3():
cnf = {'solarman' : {'port': 'FALSE'}} cnf = {'solarman' : {'port': 'FALSE'}}
TstConfig.set(cnf) err = Config.init(TstConfig(cnf), 'app/config/')
err = TstConfig.read('app/config/')
assert err == 'Config.read: Key \'solarman\' error:\nKey \'port\' error:\nint(\'FALSE\') raised ValueError("invalid literal for int() with base 10: \'FALSE\'")' 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() cnf = Config.get()
assert cnf == {'solarman': {'port': 'FALSE'}} assert cnf == {}
def test_read_cnf4(): def test_read_cnf4():
cnf = {'solarman' : {'port': 5000}} cnf = {'solarman' : {'port': 5000}}
TstConfig.set(cnf) err = Config.init(TstConfig(cnf), 'app/config/')
err = TstConfig.read('app/config/')
assert err == None 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'}, 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': { 'inverters': {
'allow_all': False, '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(): def test_read_cnf5():
cnf = {'solarman' : {'port': 1023}} cnf = {'solarman' : {'port': 1023}}
TstConfig.set(cnf) err = Config.init(TstConfig(cnf), 'app/config/')
err = TstConfig.read('app/config/')
assert err != None assert err != None
def test_read_cnf6(): def test_read_cnf6():
cnf = {'solarman' : {'port': 65536}} cnf = {'solarman' : {'port': 65536}}
TstConfig.set(cnf) err = Config.init(TstConfig(cnf), 'app/config/')
err = TstConfig.read('app/config/')
assert err != None assert err != None

View File

@@ -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': ''
}
}
}

View File

@@ -6,7 +6,7 @@ import gc
from mock import patch from mock import patch
from enum import Enum from enum import Enum
from infos import Infos from infos import Infos
from config import Config from cnf.config import Config
from gen3.talent import Talent from gen3.talent import Talent
from inverter_base import InverterBase from inverter_base import InverterBase
from singleton import Singleton from singleton import Singleton

View File

@@ -6,7 +6,7 @@ import sys,gc
from mock import patch from mock import patch
from enum import Enum from enum import Enum
from infos import Infos from infos import Infos
from config import Config from cnf.config import Config
from proxy import Proxy from proxy import Proxy
from inverter_base import InverterBase from inverter_base import InverterBase
from singleton import Singleton from singleton import Singleton

View File

@@ -5,7 +5,7 @@ import asyncio
from mock import patch from mock import patch
from enum import Enum from enum import Enum
from infos import Infos from infos import Infos
from config import Config from cnf.config import Config
from proxy import Proxy from proxy import Proxy
from inverter_base import InverterBase from inverter_base import InverterBase
from singleton import Singleton from singleton import Singleton

View File

@@ -6,7 +6,7 @@ from aiomqtt import MqttCodeError
from mock import patch from mock import patch
from enum import Enum from enum import Enum
from singleton import Singleton from singleton import Singleton
from config import Config from cnf.config import Config
from infos import Infos from infos import Infos
from mqtt import Mqtt from mqtt import Mqtt
from inverter_base import InverterBase from inverter_base import InverterBase

View File

@@ -10,7 +10,7 @@ from singleton import Singleton
from mqtt import Mqtt from mqtt import Mqtt
from modbus import Modbus from modbus import Modbus
from gen3plus.solarman_v5 import SolarmanV5 from gen3plus.solarman_v5 import SolarmanV5
from config import Config from cnf.config import Config
pytest_plugins = ('pytest_asyncio',) pytest_plugins = ('pytest_asyncio',)

View File

@@ -9,7 +9,7 @@ from singleton import Singleton
from proxy import Proxy from proxy import Proxy
from mqtt import Mqtt from mqtt import Mqtt
from gen3plus.solarman_v5 import SolarmanV5 from gen3plus.solarman_v5 import SolarmanV5
from config import Config from cnf.config import Config
pytest_plugins = ('pytest_asyncio',) pytest_plugins = ('pytest_asyncio',)

View File

@@ -7,7 +7,7 @@ import random
from math import isclose from math import isclose
from async_stream import AsyncIfcImpl, StreamPtr from async_stream import AsyncIfcImpl, StreamPtr
from gen3plus.solarman_v5 import SolarmanV5, SolarmanBase from gen3plus.solarman_v5 import SolarmanV5, SolarmanBase
from config import Config from cnf.config import Config
from infos import Infos, Register from infos import Infos, Register
from modbus import Modbus from modbus import Modbus
from messages import State, Message from messages import State, Message

View File

@@ -3,7 +3,7 @@ import pytest, logging, asyncio
from math import isclose from math import isclose
from async_stream import AsyncIfcImpl, StreamPtr from async_stream import AsyncIfcImpl, StreamPtr
from gen3.talent import Talent, Control from gen3.talent import Talent, Control
from config import Config from cnf.config import Config
from infos import Infos, Register from infos import Infos, Register
from modbus import Modbus from modbus import Modbus
from messages import State from messages import State