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'''
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()

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -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',)

View File

@@ -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',)

View File

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

View File

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