S allius/issue217 2 (#230)
* add some reader classes to get the configuration * adapt unittests * get config from json or toml file * loop over all config readers to get the configuration * rename config test files * use relative paths for coverage test in vscode * do not throw an error for missing config files * remove obsolete tests * use dotted key notation for pv sub dictonary * log config reading progress * remove create_config_toml.py * remove obsolete tests for the ha_addon * disable mosquitto tests if the server is down * ignore main method for test coverage * increase test coverage * pytest-cov: use relative_files only on github, so coverage will work with vscode locally * remove unneeded imports * add missing test cases * disable branch coverage, cause its not reachable
This commit is contained in:
@@ -1,24 +1,47 @@
|
||||
'''Config module handles the proxy configuration in the config.toml file'''
|
||||
'''Config module handles the proxy configuration'''
|
||||
|
||||
import tomllib
|
||||
import shutil
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
from schema import Schema, And, Or, Use, Optional
|
||||
|
||||
|
||||
class ConfigIfc(ABC):
|
||||
'''Abstract basis class for config readers'''
|
||||
def __init__(self):
|
||||
Config.add(self)
|
||||
|
||||
@abstractmethod
|
||||
def get_config(cls) -> dict: # pragma: no cover
|
||||
def get_config(self) -> dict: # pragma: no cover
|
||||
'''get the unverified config from the reader'''
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def descr(self) -> str: # pragma: no cover
|
||||
'''return a descriction of the source, e.g. the file name'''
|
||||
pass
|
||||
|
||||
def _extend_key(self, conf, key, val):
|
||||
'''split a dotted dict key into a hierarchical dict tree '''
|
||||
lst = key.split('.')
|
||||
d = conf
|
||||
for i, idx in enumerate(lst, 1): # pragma: no branch
|
||||
if i == len(lst):
|
||||
d[idx] = val
|
||||
break
|
||||
if idx not in d:
|
||||
d[idx] = {}
|
||||
d = d[idx]
|
||||
|
||||
|
||||
class Config():
|
||||
'''Static class Config is reads and sanitize the config.
|
||||
'''Static class Config build and sanitize the internal config dictenary.
|
||||
|
||||
Read config.toml file and sanitize it with read().
|
||||
Get named parts of the config with get()'''
|
||||
act_config = {}
|
||||
def_config = {}
|
||||
Using config readers, a partial configuration is added to config.
|
||||
Config readers are a derivation of the abstract ConfigIfc reader.
|
||||
When a config reader is instantiated, theits `get_config` method is
|
||||
called automatically and afterwards the config will be merged.
|
||||
'''
|
||||
|
||||
conf_schema = Schema({
|
||||
'tsun': {
|
||||
@@ -34,8 +57,10 @@ class Config():
|
||||
'mqtt': {
|
||||
'host': Use(str),
|
||||
'port': And(Use(int), lambda n: 1024 <= n <= 65535),
|
||||
'user': And(Use(str), Use(lambda s: s if len(s) > 0 else None)),
|
||||
'passwd': And(Use(str), Use(lambda s: s if len(s) > 0 else None))
|
||||
'user': Or(None, And(Use(str),
|
||||
Use(lambda s: s if len(s) > 0 else None))),
|
||||
'passwd': Or(None, And(Use(str),
|
||||
Use(lambda s: s if len(s) > 0 else None)))
|
||||
},
|
||||
'ha': {
|
||||
'auto_conf_prefix': Use(str),
|
||||
@@ -99,52 +124,74 @@ class Config():
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def init(cls, ifc: ConfigIfc, path='') -> None | str:
|
||||
cls.ifc = ifc
|
||||
cls.act_config = {}
|
||||
def init(cls, def_reader: ConfigIfc) -> None | str:
|
||||
'''Initialise the Proxy-Config
|
||||
|
||||
Copy the internal default config file into the config directory
|
||||
and initialise the Config with the default configuration '''
|
||||
cls.err = None
|
||||
cls.def_config = {}
|
||||
return cls.read(path)
|
||||
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
|
||||
|
||||
# read example config file as default configuration
|
||||
try:
|
||||
def_config = def_reader.get_config()
|
||||
cls.def_config = cls.conf_schema.validate(def_config)
|
||||
logging.info(f'Read from {def_reader.descr()} => ok')
|
||||
except Exception as error:
|
||||
cls.err = f'Config.read: {error}'
|
||||
logging.error(
|
||||
f"Can't read from {def_reader.descr()} => error\n {error}")
|
||||
|
||||
cls.act_config = cls.def_config.copy()
|
||||
|
||||
@classmethod
|
||||
def read(cls, path) -> None | str:
|
||||
'''Read config file, merge it with the default config
|
||||
def add(cls, reader: ConfigIfc):
|
||||
'''Merge the config from the Config Reader into the config
|
||||
|
||||
Checks if a default config exists. If no default configuration exists,
|
||||
the Config.init method has not yet been called.This is normal for the very
|
||||
first Config Reader which creates the default config and must be ignored
|
||||
here. The default config reader is handled in the Config.init method'''
|
||||
if hasattr(cls, 'def_config'):
|
||||
cls.__parse(reader)
|
||||
|
||||
@classmethod
|
||||
def get_error(cls) -> None | str:
|
||||
'''return the last error as a string or None if there is no error'''
|
||||
return cls.err
|
||||
|
||||
@classmethod
|
||||
def __parse(cls, reader) -> None | str:
|
||||
'''Read config from the reader, merge it with the default config
|
||||
and sanitize the result'''
|
||||
err = None
|
||||
config = {}
|
||||
logger = logging.getLogger('data')
|
||||
|
||||
res = 'ok'
|
||||
try:
|
||||
# read example config file as default configuration
|
||||
cls.def_config = {}
|
||||
with open(f"{path}default_config.toml", "rb") as f:
|
||||
def_config = tomllib.load(f)
|
||||
cls.def_config = cls.conf_schema.validate(def_config)
|
||||
|
||||
# overwrite the default values, with values from
|
||||
# the config.toml file
|
||||
usr_config = cls.ifc.get_config()
|
||||
|
||||
# merge the default and the user config
|
||||
config = def_config.copy()
|
||||
rd_config = reader.get_config()
|
||||
config = cls.act_config.copy()
|
||||
for key in ['tsun', 'solarman', 'mqtt', 'ha', 'inverters',
|
||||
'gen3plus']:
|
||||
if key in usr_config:
|
||||
config[key] |= usr_config[key]
|
||||
|
||||
try:
|
||||
cls.act_config = cls.conf_schema.validate(config)
|
||||
except Exception as error:
|
||||
err = f'Config.read: {error}'
|
||||
logging.error(err)
|
||||
|
||||
# logging.debug(f'Readed config: "{cls.act_config}" ')
|
||||
if key in rd_config:
|
||||
config[key] = config[key] | rd_config[key]
|
||||
|
||||
cls.act_config = cls.conf_schema.validate(config)
|
||||
except FileNotFoundError:
|
||||
res = 'n/a'
|
||||
except Exception as error:
|
||||
err = f'Config.read: {error}'
|
||||
logger.error(err)
|
||||
cls.act_config = {}
|
||||
cls.err = f'error: {error}'
|
||||
logging.error(
|
||||
f"Can't read from {reader.descr()} => error\n {error}")
|
||||
|
||||
return err
|
||||
logging.info(f'Read from {reader.descr()} => {res}')
|
||||
return cls.err
|
||||
|
||||
@classmethod
|
||||
def get(cls, member: str = None):
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
'''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
|
||||
25
app/src/cnf/config_read_env.py
Normal file
25
app/src/cnf/config_read_env.py
Normal file
@@ -0,0 +1,25 @@
|
||||
'''Config Reader module which handles config values from the environment'''
|
||||
|
||||
import os
|
||||
from cnf.config import ConfigIfc
|
||||
|
||||
|
||||
class ConfigReadEnv(ConfigIfc):
|
||||
'''Reader for environment values of the configuration'''
|
||||
|
||||
def get_config(self) -> dict:
|
||||
conf = {}
|
||||
data = [
|
||||
('mqtt.host', 'MQTT_HOST'),
|
||||
('mqtt.port', 'MQTT_PORT'),
|
||||
('mqtt.user', 'MQTT_USER'),
|
||||
('mqtt.passwd', 'MQTT_PASSWORD'),
|
||||
]
|
||||
for key, env_var in data:
|
||||
val = os.getenv(env_var)
|
||||
if val:
|
||||
self._extend_key(conf, key, val)
|
||||
return conf
|
||||
|
||||
def descr(self):
|
||||
return "Read environment"
|
||||
46
app/src/cnf/config_read_json.py
Normal file
46
app/src/cnf/config_read_json.py
Normal file
@@ -0,0 +1,46 @@
|
||||
'''Config Reader module which handles *.json config files'''
|
||||
|
||||
import json
|
||||
from cnf.config import ConfigIfc
|
||||
|
||||
|
||||
class ConfigReadJson(ConfigIfc):
|
||||
'''Reader for json config files'''
|
||||
def __init__(self, cnf_file='/data/options.json'):
|
||||
'''Read a json file and add the settings to the config'''
|
||||
if not isinstance(cnf_file, str):
|
||||
return
|
||||
self.cnf_file = cnf_file
|
||||
super().__init__()
|
||||
|
||||
def convert_inv(self, conf, inv):
|
||||
if 'serial' in inv:
|
||||
snr = inv['serial']
|
||||
del inv['serial']
|
||||
conf[snr] = {}
|
||||
|
||||
for key, val in inv.items():
|
||||
self._extend_key(conf[snr], key, val)
|
||||
|
||||
def convert_inv_arr(self, conf, key, val: list):
|
||||
if key not in conf:
|
||||
conf[key] = {}
|
||||
for elm in val:
|
||||
self.convert_inv(conf[key], elm)
|
||||
|
||||
def convert_to_obj(self, data):
|
||||
conf = {}
|
||||
for key, val in data.items():
|
||||
if key == 'inverters' and isinstance(val, list):
|
||||
self.convert_inv_arr(conf, key, val)
|
||||
else:
|
||||
self._extend_key(conf, key, val)
|
||||
return conf
|
||||
|
||||
def get_config(self) -> dict:
|
||||
with open(self.cnf_file) as f:
|
||||
data = json.load(f)
|
||||
return self.convert_to_obj(data)
|
||||
|
||||
def descr(self):
|
||||
return self.cnf_file
|
||||
21
app/src/cnf/config_read_toml.py
Normal file
21
app/src/cnf/config_read_toml.py
Normal file
@@ -0,0 +1,21 @@
|
||||
'''Config Reader module which handles *.toml config files'''
|
||||
|
||||
import tomllib
|
||||
from cnf.config import ConfigIfc
|
||||
|
||||
|
||||
class ConfigReadToml(ConfigIfc):
|
||||
'''Reader for toml config files'''
|
||||
def __init__(self, cnf_file):
|
||||
'''Read a toml file and add the settings to the config'''
|
||||
if not isinstance(cnf_file, str):
|
||||
return
|
||||
self.cnf_file = cnf_file
|
||||
super().__init__()
|
||||
|
||||
def get_config(self) -> dict:
|
||||
with open(self.cnf_file, "rb") as f:
|
||||
return tomllib.load(f)
|
||||
|
||||
def descr(self):
|
||||
return self.cnf_file
|
||||
@@ -2,6 +2,7 @@ import logging
|
||||
import asyncio
|
||||
import signal
|
||||
import os
|
||||
import argparse
|
||||
from asyncio import StreamReader, StreamWriter
|
||||
from aiohttp import web
|
||||
from logging import config # noqa F401
|
||||
@@ -11,7 +12,9 @@ from gen3.inverter_g3 import InverterG3
|
||||
from gen3plus.inverter_g3p import InverterG3P
|
||||
from scheduler import Schedule
|
||||
from cnf.config import Config
|
||||
from cnf.config_ifc_proxy import ConfigIfcProxy
|
||||
from cnf.config_read_env import ConfigReadEnv
|
||||
from cnf.config_read_toml import ConfigReadToml
|
||||
from cnf.config_read_json import ConfigReadJson
|
||||
from modbus_tcp import ModbusTcp
|
||||
|
||||
routes = web.RouteTableDef()
|
||||
@@ -117,6 +120,8 @@ def get_log_level() -> int:
|
||||
'''checks if LOG_LVL is set in the environment and returns the
|
||||
corresponding logging.LOG_LEVEL'''
|
||||
log_level = os.getenv('LOG_LVL', 'INFO')
|
||||
logging.info(f"LOG_LVL : {log_level}")
|
||||
|
||||
if log_level == 'DEBUG':
|
||||
log_level = logging.DEBUG
|
||||
elif log_level == 'WARN':
|
||||
@@ -126,7 +131,17 @@ def get_log_level() -> int:
|
||||
return log_level
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-p', '--config_path', type=str,
|
||||
default='./config/',
|
||||
help='set path for the configuration files')
|
||||
parser.add_argument('-j', '--json_config', type=str,
|
||||
help='read user config from json-file')
|
||||
parser.add_argument('-t', '--toml_config', type=str,
|
||||
help='read user config from toml-file')
|
||||
parser.add_argument('--add_on', action='store_true')
|
||||
args = parser.parse_args()
|
||||
#
|
||||
# Setup our daily, rotating logger
|
||||
#
|
||||
@@ -135,9 +150,14 @@ if __name__ == "__main__":
|
||||
|
||||
logging.config.fileConfig('logging.ini')
|
||||
logging.info(f'Server "{serv_name} - {version}" will be started')
|
||||
logging.info(f"AddOn: {args.add_on}")
|
||||
logging.info(f"config_path: {args.config_path}")
|
||||
logging.info(f"json_config: {args.json_config}")
|
||||
logging.info(f"toml_config: {args.toml_config}")
|
||||
log_level = get_log_level()
|
||||
logging.info('******')
|
||||
|
||||
# set lowest-severity for 'root', 'msg', 'conn' and 'data' logger
|
||||
log_level = get_log_level()
|
||||
logging.getLogger().setLevel(log_level)
|
||||
logging.getLogger('msg').setLevel(log_level)
|
||||
logging.getLogger('conn').setLevel(log_level)
|
||||
@@ -150,9 +170,18 @@ if __name__ == "__main__":
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
# read config file
|
||||
ConfigErr = Config.init(ConfigIfcProxy())
|
||||
Config.init(ConfigReadToml("default_config.toml"))
|
||||
ConfigReadEnv()
|
||||
ConfigReadJson(args.config_path + "config.json")
|
||||
ConfigReadToml(args.config_path + "config.toml")
|
||||
ConfigReadJson(args.json_config)
|
||||
ConfigReadToml(args.toml_config)
|
||||
ConfigErr = Config.get_error()
|
||||
|
||||
if ConfigErr is not None:
|
||||
logging.info(f'ConfigErr: {ConfigErr}')
|
||||
logging.info('******')
|
||||
|
||||
Proxy.class_init()
|
||||
Schedule.start()
|
||||
ModbusTcp(loop)
|
||||
|
||||
@@ -1,8 +1,47 @@
|
||||
# test_with_pytest.py
|
||||
import pytest
|
||||
import tomllib
|
||||
import json
|
||||
from mock import patch
|
||||
from schema import SchemaMissingKeyError
|
||||
from cnf.config import Config, ConfigIfc
|
||||
from cnf.config_read_toml import ConfigReadToml
|
||||
|
||||
class FakeBuffer:
|
||||
rd = str()
|
||||
|
||||
test_buffer = FakeBuffer
|
||||
|
||||
|
||||
class FakeFile():
|
||||
def __init__(self):
|
||||
self.buf = test_buffer
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
pass
|
||||
|
||||
|
||||
class FakeOptionsFile(FakeFile):
|
||||
def __init__(self, OpenTextMode):
|
||||
super().__init__()
|
||||
self.bin_mode = 'b' in OpenTextMode
|
||||
|
||||
def read(self):
|
||||
if self.bin_mode:
|
||||
return bytearray(self.buf.rd.encode('utf-8')).copy()
|
||||
else:
|
||||
return self.buf.rd.copy()
|
||||
|
||||
def patch_open():
|
||||
def new_open(file: str, OpenTextMode="rb"):
|
||||
if file == "_no__file__no_":
|
||||
raise FileNotFoundError
|
||||
return FakeOptionsFile(OpenTextMode)
|
||||
|
||||
with patch('builtins.open', new_open) as conn:
|
||||
yield conn
|
||||
|
||||
class TstConfig(ConfigIfc):
|
||||
|
||||
@@ -11,7 +50,7 @@ class TstConfig(ConfigIfc):
|
||||
cls.act_config = cnf
|
||||
|
||||
@classmethod
|
||||
def get_config(cls) -> dict:
|
||||
def add_config(cls) -> dict:
|
||||
return cls.act_config
|
||||
|
||||
|
||||
@@ -23,6 +62,40 @@ def test_empty_config():
|
||||
except SchemaMissingKeyError:
|
||||
pass
|
||||
|
||||
@pytest.fixture
|
||||
def ConfigDefault():
|
||||
return {'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,
|
||||
'R170000000000001': {
|
||||
'suggested_area': '',
|
||||
'modbus_polling': False,
|
||||
'monitor_sn': 0,
|
||||
'node_id': '',
|
||||
'pv1': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-395M'},
|
||||
'pv2': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-395M'},
|
||||
'sensor_list': 688
|
||||
},
|
||||
'Y170000000000001': {
|
||||
'modbus_polling': True,
|
||||
'monitor_sn': 2000000000,
|
||||
'suggested_area': '',
|
||||
'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'},
|
||||
'sensor_list': 688
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def ConfigComplete():
|
||||
return {
|
||||
@@ -70,38 +143,9 @@ def ConfigComplete():
|
||||
}
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def ConfigMinimum():
|
||||
return {
|
||||
'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': True,
|
||||
'R170000000000001': {'node_id': '',
|
||||
'modbus_polling': True,
|
||||
'monitor_sn': 0,
|
||||
'suggested_area': '',
|
||||
'sensor_list': 688}}}
|
||||
|
||||
|
||||
def test_default_config():
|
||||
with open("app/config/default_config.toml", "rb") as f:
|
||||
cnf = tomllib.load(f)
|
||||
|
||||
try:
|
||||
validated = Config.conf_schema.validate(cnf)
|
||||
except Exception:
|
||||
assert False
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
validated = Config.def_config
|
||||
assert validated == {'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,
|
||||
@@ -146,76 +190,53 @@ def test_full_config(ConfigComplete):
|
||||
assert False
|
||||
assert validated == ConfigComplete
|
||||
|
||||
def test_mininum_config(ConfigMinimum):
|
||||
cnf = {'tsun': {'enabled': True, 'host': 'logger.talent-monitoring.com', 'port': 5005},
|
||||
'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+']},
|
||||
'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE']}}},
|
||||
'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': True,
|
||||
'R170000000000001': {}}
|
||||
}
|
||||
def test_read_empty(ConfigDefault):
|
||||
test_buffer.rd = ""
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadToml("config/config.toml")
|
||||
err = Config.get_error()
|
||||
|
||||
try:
|
||||
validated = Config.conf_schema.validate(cnf)
|
||||
except Exception:
|
||||
assert False
|
||||
assert validated == ConfigMinimum
|
||||
|
||||
def test_read_empty():
|
||||
cnf = {}
|
||||
err = Config.init(TstConfig(cnf), 'app/config/')
|
||||
assert err == None
|
||||
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,
|
||||
'R170000000000001': {
|
||||
'suggested_area': '',
|
||||
'modbus_polling': False,
|
||||
'monitor_sn': 0,
|
||||
'node_id': '',
|
||||
'pv1': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-395M'},
|
||||
'pv2': {'manufacturer': 'Risen',
|
||||
'type': 'RSM40-8-395M'},
|
||||
'sensor_list': 688
|
||||
},
|
||||
'Y170000000000001': {
|
||||
'modbus_polling': True,
|
||||
'monitor_sn': 2000000000,
|
||||
'suggested_area': '',
|
||||
'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'},
|
||||
'sensor_list': 688
|
||||
}
|
||||
}
|
||||
}
|
||||
assert cnf == ConfigDefault
|
||||
|
||||
defcnf = Config.def_config.get('solarman')
|
||||
assert defcnf == {'enabled': True, 'host': 'iot.talent-monitoring.com', 'port': 10000}
|
||||
assert True == Config.is_default('solarman')
|
||||
|
||||
def test_no_file():
|
||||
cnf = {}
|
||||
err = Config.init(TstConfig(cnf), '')
|
||||
Config.init(ConfigReadToml("default_config.toml"))
|
||||
err = Config.get_error()
|
||||
assert err == "Config.read: [Errno 2] No such file or directory: 'default_config.toml'"
|
||||
cnf = Config.get()
|
||||
assert cnf == {}
|
||||
defcnf = Config.def_config.get('solarman')
|
||||
assert defcnf == None
|
||||
|
||||
def test_no_file2():
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
assert Config.err == None
|
||||
ConfigReadToml("_no__file__no_")
|
||||
err = Config.get_error()
|
||||
assert err == None
|
||||
|
||||
def test_invalid_filename():
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
assert Config.err == None
|
||||
ConfigReadToml(None)
|
||||
err = Config.get_error()
|
||||
assert err == None
|
||||
|
||||
def test_read_cnf1():
|
||||
cnf = {'solarman' : {'enabled': False}}
|
||||
err = Config.init(TstConfig(cnf), 'app/config/')
|
||||
test_buffer.rd = "solarman.enabled = false"
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadToml("config/config.toml")
|
||||
err = Config.get_error()
|
||||
|
||||
assert err == None
|
||||
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'},
|
||||
@@ -256,8 +277,13 @@ def test_read_cnf1():
|
||||
assert False == Config.is_default('solarman')
|
||||
|
||||
def test_read_cnf2():
|
||||
cnf = {'solarman' : {'enabled': 'FALSE'}}
|
||||
err = Config.init(TstConfig(cnf), 'app/config/')
|
||||
test_buffer.rd = "solarman.enabled = 'FALSE'"
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadToml("config/config.toml")
|
||||
err = Config.get_error()
|
||||
|
||||
assert err == None
|
||||
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'},
|
||||
@@ -293,16 +319,26 @@ def test_read_cnf2():
|
||||
}
|
||||
assert True == Config.is_default('solarman')
|
||||
|
||||
def test_read_cnf3():
|
||||
cnf = {'solarman' : {'port': 'FALSE'}}
|
||||
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\'")'
|
||||
def test_read_cnf3(ConfigDefault):
|
||||
test_buffer.rd = "solarman.port = 'FALSE'"
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadToml("config/config.toml")
|
||||
err = Config.get_error()
|
||||
|
||||
assert err == 'error: Key \'solarman\' error:\nKey \'port\' error:\nint(\'FALSE\') raised ValueError("invalid literal for int() with base 10: \'FALSE\'")'
|
||||
cnf = Config.get()
|
||||
assert cnf == {}
|
||||
assert cnf == ConfigDefault
|
||||
|
||||
def test_read_cnf4():
|
||||
cnf = {'solarman' : {'port': 5000}}
|
||||
err = Config.init(TstConfig(cnf), 'app/config/')
|
||||
test_buffer.rd = "solarman.port = 5000"
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadToml("config/config.toml")
|
||||
err = Config.get_error()
|
||||
|
||||
assert err == None
|
||||
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'},
|
||||
@@ -339,11 +375,19 @@ def test_read_cnf4():
|
||||
assert False == Config.is_default('solarman')
|
||||
|
||||
def test_read_cnf5():
|
||||
cnf = {'solarman' : {'port': 1023}}
|
||||
err = Config.init(TstConfig(cnf), 'app/config/')
|
||||
test_buffer.rd = "solarman.port = 1023"
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadToml("config/config.toml")
|
||||
err = Config.get_error()
|
||||
assert err != None
|
||||
|
||||
def test_read_cnf6():
|
||||
cnf = {'solarman' : {'port': 65536}}
|
||||
err = Config.init(TstConfig(cnf), 'app/config/')
|
||||
test_buffer.rd = "solarman.port = 65536"
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadToml("config/config.toml")
|
||||
err = Config.get_error()
|
||||
assert err != None
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
# 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': ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
53
app/tests/test_config_read_env.py
Normal file
53
app/tests/test_config_read_env.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# test_with_pytest.py
|
||||
import pytest
|
||||
import os
|
||||
from mock import patch
|
||||
from cnf.config import Config
|
||||
from cnf.config_read_toml import ConfigReadToml
|
||||
from cnf.config_read_env import ConfigReadEnv
|
||||
|
||||
def patch_getenv():
|
||||
def new_getenv(key: str, defval=None):
|
||||
"""Get an environment variable, return None if it doesn't exist.
|
||||
The optional second argument can specify an alternate default. key,
|
||||
default and the result are str."""
|
||||
if key == 'MQTT_PASSWORD':
|
||||
return 'passwd'
|
||||
elif key == 'MQTT_PORT':
|
||||
return 1234
|
||||
elif key == 'MQTT_HOST':
|
||||
return ""
|
||||
return defval
|
||||
|
||||
with patch.object(os, 'getenv', new_getenv) as conn:
|
||||
yield conn
|
||||
|
||||
def test_extend_key():
|
||||
cnf_rd = ConfigReadEnv()
|
||||
|
||||
conf = {}
|
||||
cnf_rd._extend_key(conf, "mqtt.user", "testuser")
|
||||
assert conf == {
|
||||
'mqtt': {
|
||||
'user': 'testuser',
|
||||
},
|
||||
}
|
||||
|
||||
conf = {}
|
||||
cnf_rd._extend_key(conf, "mqtt", "testuser")
|
||||
assert conf == {
|
||||
'mqtt': 'testuser',
|
||||
}
|
||||
|
||||
conf = {}
|
||||
cnf_rd._extend_key(conf, "", "testuser")
|
||||
assert conf == {'': 'testuser'}
|
||||
|
||||
def test_read_env_config():
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
assert Config.get('mqtt') == {'host': 'mqtt', 'port': 1883, 'user': None, 'passwd': None}
|
||||
for _ in patch_getenv():
|
||||
|
||||
ConfigReadEnv()
|
||||
assert Config.get_error() == None
|
||||
assert Config.get('mqtt') == {'host': 'mqtt', 'port': 1234, 'user': None, 'passwd': 'passwd'}
|
||||
404
app/tests/test_config_read_json.py
Normal file
404
app/tests/test_config_read_json.py
Normal file
@@ -0,0 +1,404 @@
|
||||
# test_with_pytest.py
|
||||
import pytest
|
||||
from mock import patch
|
||||
from cnf.config import Config
|
||||
from cnf.config_read_json import ConfigReadJson
|
||||
from cnf.config_read_toml import ConfigReadToml
|
||||
|
||||
from test_config import ConfigDefault, ConfigComplete
|
||||
|
||||
|
||||
class CnfIfc(ConfigReadJson):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
|
||||
class FakeBuffer:
|
||||
rd = str()
|
||||
wr = str()
|
||||
|
||||
|
||||
test_buffer = FakeBuffer
|
||||
|
||||
|
||||
class FakeFile():
|
||||
def __init__(self):
|
||||
self.buf = test_buffer
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
pass
|
||||
|
||||
|
||||
class FakeOptionsFile(FakeFile):
|
||||
def __init__(self, OpenTextMode):
|
||||
super().__init__()
|
||||
self.bin_mode = 'b' in OpenTextMode
|
||||
|
||||
def read(self):
|
||||
print(f"Fake.read: bmode:{self.bin_mode}")
|
||||
if self.bin_mode:
|
||||
return bytearray(self.buf.rd.encode('utf-8')).copy()
|
||||
else:
|
||||
print(f"Fake.read: str:{self.buf.rd}")
|
||||
return self.buf.rd
|
||||
|
||||
def patch_open():
|
||||
def new_open(file: str, OpenTextMode="r"):
|
||||
if file == "_no__file__no_":
|
||||
raise FileNotFoundError
|
||||
return FakeOptionsFile(OpenTextMode)
|
||||
|
||||
with patch('builtins.open', new_open) as conn:
|
||||
yield conn
|
||||
|
||||
@pytest.fixture
|
||||
def ConfigTomlEmpty():
|
||||
return {
|
||||
'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'},
|
||||
'solarman': {
|
||||
'enabled': True,
|
||||
'host': 'iot.talent-monitoring.com',
|
||||
'port': 10000,
|
||||
},
|
||||
'tsun': {
|
||||
'enabled': True,
|
||||
'host': 'logger.talent-monitoring.com',
|
||||
'port': 5005,
|
||||
},
|
||||
'inverters': {
|
||||
'allow_all': False
|
||||
},
|
||||
'gen3plus': {'at_acl': {'tsun': {'allow': [], 'block': []},
|
||||
'mqtt': {'allow': [], 'block': []}}},
|
||||
}
|
||||
|
||||
|
||||
def test_no_config(ConfigDefault):
|
||||
test_buffer.rd = "" # empty buffer, no json
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadJson()
|
||||
err = Config.get_error()
|
||||
|
||||
assert err == 'error: Expecting value: line 1 column 1 (char 0)'
|
||||
cnf = Config.get()
|
||||
assert cnf == ConfigDefault
|
||||
|
||||
def test_no_file(ConfigDefault):
|
||||
test_buffer.rd = "" # empty buffer, no json
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadJson("_no__file__no_")
|
||||
err = Config.get_error()
|
||||
|
||||
assert err == None
|
||||
cnf = Config.get()
|
||||
assert cnf == ConfigDefault
|
||||
|
||||
def test_invalid_filename(ConfigDefault):
|
||||
test_buffer.rd = "" # empty buffer, no json
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadJson(None)
|
||||
err = Config.get_error()
|
||||
|
||||
assert err == None
|
||||
cnf = Config.get()
|
||||
assert cnf == ConfigDefault
|
||||
|
||||
def test_cnv1():
|
||||
"""test dotted key converting"""
|
||||
tst = {
|
||||
"gen3plus.at_acl.mqtt.block": [
|
||||
"AT+SUPDATE",
|
||||
"AT+"
|
||||
]
|
||||
}
|
||||
|
||||
cnf = ConfigReadJson()
|
||||
obj = cnf.convert_to_obj(tst)
|
||||
assert obj == {
|
||||
'gen3plus': {
|
||||
'at_acl': {
|
||||
'mqtt': {
|
||||
'block': [
|
||||
'AT+SUPDATE',
|
||||
"AT+"
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
def test_cnv2():
|
||||
"""test a valid list with serials in inverters"""
|
||||
tst = {
|
||||
"inverters": [
|
||||
{
|
||||
"serial": "R170000000000001",
|
||||
},
|
||||
{
|
||||
"serial": "Y170000000000001",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
cnf = ConfigReadJson()
|
||||
obj = cnf.convert_to_obj(tst)
|
||||
assert obj == {
|
||||
'inverters': {
|
||||
'R170000000000001': {},
|
||||
'Y170000000000001': {}
|
||||
},
|
||||
}
|
||||
|
||||
def test_cnv3():
|
||||
"""test the combination of a list and a scalar in inverters"""
|
||||
tst = {
|
||||
"inverters": [
|
||||
{
|
||||
"serial": "R170000000000001",
|
||||
},
|
||||
{
|
||||
"serial": "Y170000000000001",
|
||||
}
|
||||
],
|
||||
"inverters.allow_all": False,
|
||||
}
|
||||
|
||||
cnf = ConfigReadJson()
|
||||
obj = cnf.convert_to_obj(tst)
|
||||
assert obj == {
|
||||
'inverters': {
|
||||
'R170000000000001': {},
|
||||
'Y170000000000001': {},
|
||||
'allow_all': False,
|
||||
},
|
||||
}
|
||||
|
||||
def test_cnv4():
|
||||
tst = {
|
||||
"inverters": [
|
||||
{
|
||||
"serial": "R170000000000001",
|
||||
"node_id": "PV-Garage/",
|
||||
"suggested_area": "Garage",
|
||||
"modbus_polling": False,
|
||||
"pv1_manufacturer": "man1",
|
||||
"pv1_type": "type1",
|
||||
"pv2_manufacturer": "man2",
|
||||
"pv2_type": "type2",
|
||||
"sensor_list": 688
|
||||
},
|
||||
{
|
||||
"serial": "Y170000000000001",
|
||||
"monitor_sn": 2000000000,
|
||||
"node_id": "PV-Garage2/",
|
||||
"suggested_area": "Garage2",
|
||||
"modbus_polling": True,
|
||||
"client_mode_host": "InverterIP",
|
||||
"client_mode_port": 1234,
|
||||
"pv1_manufacturer": "man1",
|
||||
"pv1_type": "type1",
|
||||
"pv2_manufacturer": "man2",
|
||||
"pv2_type": "type2",
|
||||
"pv3_manufacturer": "man3",
|
||||
"pv3_type": "type3",
|
||||
"pv4_manufacturer": "man4",
|
||||
"pv4_type": "type4",
|
||||
"sensor_list": 688
|
||||
}
|
||||
],
|
||||
"tsun.enabled": True,
|
||||
"solarman.enabled": True,
|
||||
"inverters.allow_all": False,
|
||||
"gen3plus.at_acl.tsun.allow": [
|
||||
"AT+Z",
|
||||
"AT+UPURL",
|
||||
"AT+SUPDATE"
|
||||
],
|
||||
"gen3plus.at_acl.tsun.block": [
|
||||
"AT+SUPDATE"
|
||||
],
|
||||
"gen3plus.at_acl.mqtt.allow": [
|
||||
"AT+"
|
||||
],
|
||||
"gen3plus.at_acl.mqtt.block": [
|
||||
"AT+SUPDATE"
|
||||
]
|
||||
}
|
||||
|
||||
cnf = ConfigReadJson()
|
||||
obj = cnf.convert_to_obj(tst)
|
||||
assert obj == {
|
||||
'gen3plus': {'at_acl': {'mqtt': {'allow': ['AT+'], 'block': ['AT+SUPDATE']},
|
||||
'tsun': {'allow': ['AT+Z', 'AT+UPURL', 'AT+SUPDATE'],
|
||||
'block': ['AT+SUPDATE']}}},
|
||||
'inverters': {'R170000000000001': {'modbus_polling': False,
|
||||
'node_id': 'PV-Garage/',
|
||||
'pv1_manufacturer': 'man1',
|
||||
'pv1_type': 'type1',
|
||||
'pv2_manufacturer': 'man2',
|
||||
'pv2_type': 'type2',
|
||||
'sensor_list': 688,
|
||||
'suggested_area': 'Garage'},
|
||||
'Y170000000000001': {'client_mode_host': 'InverterIP',
|
||||
'client_mode_port': 1234,
|
||||
'modbus_polling': True,
|
||||
'monitor_sn': 2000000000,
|
||||
'node_id': 'PV-Garage2/',
|
||||
'pv1_manufacturer': 'man1',
|
||||
'pv1_type': 'type1',
|
||||
'pv2_manufacturer': 'man2',
|
||||
'pv2_type': 'type2',
|
||||
'pv3_manufacturer': 'man3',
|
||||
'pv3_type': 'type3',
|
||||
'pv4_manufacturer': 'man4',
|
||||
'pv4_type': 'type4',
|
||||
'sensor_list': 688,
|
||||
'suggested_area': 'Garage2'},
|
||||
'allow_all': False},
|
||||
'solarman': {'enabled': True},
|
||||
'tsun': {'enabled': True}
|
||||
}
|
||||
|
||||
def test_cnv5():
|
||||
"""test a invalid list with missing serials"""
|
||||
tst = {
|
||||
"inverters": [
|
||||
{
|
||||
"node_id": "PV-Garage1/",
|
||||
},
|
||||
{
|
||||
"serial": "Y170000000000001",
|
||||
"node_id": "PV-Garage2/",
|
||||
}
|
||||
],
|
||||
}
|
||||
cnf = ConfigReadJson()
|
||||
obj = cnf.convert_to_obj(tst)
|
||||
assert obj == {
|
||||
'inverters': {
|
||||
'Y170000000000001': {'node_id': 'PV-Garage2/'}
|
||||
},
|
||||
}
|
||||
|
||||
def test_cnv6():
|
||||
"""test overwritting a value in inverters"""
|
||||
tst = {
|
||||
"inverters": [{
|
||||
"serial": "Y170000000000001",
|
||||
"node_id": "PV-Garage2/",
|
||||
}],
|
||||
}
|
||||
tst2 = {
|
||||
"inverters": [{
|
||||
"serial": "Y170000000000001",
|
||||
"node_id": "PV-Garden/",
|
||||
}],
|
||||
}
|
||||
cnf = ConfigReadJson()
|
||||
conf = {}
|
||||
for key, val in tst.items():
|
||||
cnf.convert_inv_arr(conf, key, val)
|
||||
|
||||
assert conf == {
|
||||
'inverters': {
|
||||
'Y170000000000001': {'node_id': 'PV-Garage2/'}
|
||||
},
|
||||
}
|
||||
|
||||
for key, val in tst2.items():
|
||||
cnf.convert_inv_arr(conf, key, val)
|
||||
|
||||
assert conf == {
|
||||
'inverters': {
|
||||
'Y170000000000001': {'node_id': 'PV-Garden/'}
|
||||
},
|
||||
}
|
||||
|
||||
def test_empty_config(ConfigDefault):
|
||||
test_buffer.rd = "{}" # empty json
|
||||
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadJson()
|
||||
err = Config.get_error()
|
||||
|
||||
assert err == None
|
||||
cnf = Config.get()
|
||||
assert cnf == ConfigDefault
|
||||
|
||||
|
||||
def test_full_config(ConfigComplete):
|
||||
test_buffer.rd = """
|
||||
{
|
||||
"inverters": [
|
||||
{
|
||||
"serial": "R170000000000001",
|
||||
"node_id": "PV-Garage/",
|
||||
"suggested_area": "Garage",
|
||||
"modbus_polling": false,
|
||||
"pv1.manufacturer": "man1",
|
||||
"pv1.type": "type1",
|
||||
"pv2.manufacturer": "man2",
|
||||
"pv2.type": "type2",
|
||||
"sensor_list": 688
|
||||
},
|
||||
{
|
||||
"serial": "Y170000000000001",
|
||||
"monitor_sn": 2000000000,
|
||||
"node_id": "PV-Garage2/",
|
||||
"suggested_area": "Garage2",
|
||||
"modbus_polling": true,
|
||||
"client_mode_host": "InverterIP",
|
||||
"client_mode_port": 1234,
|
||||
"pv1.manufacturer": "man1",
|
||||
"pv1.type": "type1",
|
||||
"pv2.manufacturer": "man2",
|
||||
"pv2.type": "type2",
|
||||
"pv3.manufacturer": "man3",
|
||||
"pv3.type": "type3",
|
||||
"pv4.manufacturer": "man4",
|
||||
"pv4.type": "type4",
|
||||
"sensor_list": 688
|
||||
}
|
||||
],
|
||||
"tsun.enabled": true,
|
||||
"solarman.enabled": true,
|
||||
"inverters.allow_all": false,
|
||||
"gen3plus.at_acl.tsun.allow": [
|
||||
"AT+Z",
|
||||
"AT+UPURL",
|
||||
"AT+SUPDATE"
|
||||
],
|
||||
"gen3plus.at_acl.tsun.block": [
|
||||
"AT+SUPDATE"
|
||||
],
|
||||
"gen3plus.at_acl.mqtt.allow": [
|
||||
"AT+"
|
||||
],
|
||||
"gen3plus.at_acl.mqtt.block": [
|
||||
"AT+SUPDATE"
|
||||
]
|
||||
}
|
||||
"""
|
||||
Config.init(ConfigReadToml("app/config/default_config.toml"))
|
||||
for _ in patch_open():
|
||||
ConfigReadJson()
|
||||
err = Config.get_error()
|
||||
|
||||
assert err == None
|
||||
cnf = Config.get()
|
||||
assert cnf == ConfigComplete
|
||||
@@ -12,6 +12,8 @@ from modbus import Modbus
|
||||
from gen3plus.solarman_v5 import SolarmanV5
|
||||
from cnf.config import Config
|
||||
|
||||
NO_MOSQUITTO_TEST = False
|
||||
'''disable all tests with connections to test.mosquitto.org'''
|
||||
|
||||
pytest_plugins = ('pytest_asyncio',)
|
||||
|
||||
@@ -69,8 +71,12 @@ def spy_modbus_cmd_client():
|
||||
|
||||
def test_native_client(test_hostname, test_port):
|
||||
"""Sanity check: Make sure the paho-mqtt client can connect to the test
|
||||
MQTT server.
|
||||
MQTT server. Otherwise the test set NO_MOSQUITTO_TEST to True and disable
|
||||
all test cases which depends on the test.mosquitto.org server
|
||||
"""
|
||||
global NO_MOSQUITTO_TEST
|
||||
if NO_MOSQUITTO_TEST:
|
||||
pytest.skip('skipping, since Mosquitto is not reliable at the moment')
|
||||
|
||||
import paho.mqtt.client as mqtt
|
||||
import threading
|
||||
@@ -82,10 +88,62 @@ def test_native_client(test_hostname, test_port):
|
||||
on_connect = threading.Event()
|
||||
c.on_connect = Mock(side_effect=lambda *_: on_connect.set())
|
||||
c.connect_async(test_hostname, test_port)
|
||||
assert on_connect.wait(10)
|
||||
if not on_connect.wait(3):
|
||||
NO_MOSQUITTO_TEST = True # skip all mosquitto tests
|
||||
pytest.skip('skipping, since Mosquitto is not reliable at the moment')
|
||||
finally:
|
||||
c.loop_stop()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mqtt_connection(config_mqtt_conn):
|
||||
global NO_MOSQUITTO_TEST
|
||||
if NO_MOSQUITTO_TEST:
|
||||
pytest.skip('skipping, since Mosquitto is not reliable at the moment')
|
||||
|
||||
_ = config_mqtt_conn
|
||||
assert asyncio.get_running_loop()
|
||||
|
||||
on_connect = asyncio.Event()
|
||||
async def cb():
|
||||
on_connect.set()
|
||||
|
||||
try:
|
||||
m = Mqtt(cb)
|
||||
assert m.task
|
||||
assert await asyncio.wait_for(on_connect.wait(), 5)
|
||||
# await asyncio.sleep(1)
|
||||
assert 0 == m.ha_restarts
|
||||
await m.publish('homeassistant/status', 'online')
|
||||
except TimeoutError:
|
||||
assert False
|
||||
finally:
|
||||
await m.close()
|
||||
await m.publish('homeassistant/status', 'online')
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ha_reconnect(config_mqtt_conn):
|
||||
global NO_MOSQUITTO_TEST
|
||||
if NO_MOSQUITTO_TEST:
|
||||
pytest.skip('skipping, since Mosquitto is not reliable at the moment')
|
||||
|
||||
_ = config_mqtt_conn
|
||||
on_connect = asyncio.Event()
|
||||
async def cb():
|
||||
on_connect.set()
|
||||
|
||||
try:
|
||||
m = Mqtt(cb)
|
||||
msg = aiomqtt.Message(topic= 'homeassistant/status', payload= b'offline', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
assert not on_connect.is_set()
|
||||
|
||||
msg = aiomqtt.Message(topic= 'homeassistant/status', payload= b'online', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
assert on_connect.is_set()
|
||||
|
||||
finally:
|
||||
await m.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mqtt_no_config(config_no_conn):
|
||||
_ = config_no_conn
|
||||
@@ -110,29 +168,6 @@ async def test_mqtt_no_config(config_no_conn):
|
||||
finally:
|
||||
await m.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mqtt_connection(config_mqtt_conn):
|
||||
_ = config_mqtt_conn
|
||||
assert asyncio.get_running_loop()
|
||||
|
||||
on_connect = asyncio.Event()
|
||||
async def cb():
|
||||
on_connect.set()
|
||||
|
||||
try:
|
||||
m = Mqtt(cb)
|
||||
assert m.task
|
||||
assert await asyncio.wait_for(on_connect.wait(), 5)
|
||||
# await asyncio.sleep(1)
|
||||
assert 0 == m.ha_restarts
|
||||
await m.publish('homeassistant/status', 'online')
|
||||
except TimeoutError:
|
||||
assert False
|
||||
finally:
|
||||
await m.close()
|
||||
await m.publish('homeassistant/status', 'online')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_msg_dispatch(config_mqtt_conn, spy_modbus_cmd):
|
||||
_ = config_mqtt_conn
|
||||
@@ -209,26 +244,6 @@ async def test_msg_ignore_client_conn(config_mqtt_conn, spy_modbus_cmd_client):
|
||||
finally:
|
||||
await m.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ha_reconnect(config_mqtt_conn):
|
||||
_ = config_mqtt_conn
|
||||
on_connect = asyncio.Event()
|
||||
async def cb():
|
||||
on_connect.set()
|
||||
|
||||
try:
|
||||
m = Mqtt(cb)
|
||||
msg = aiomqtt.Message(topic= 'homeassistant/status', payload= b'offline', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
assert not on_connect.is_set()
|
||||
|
||||
msg = aiomqtt.Message(topic= 'homeassistant/status', payload= b'online', qos= 0, retain = False, mid= 0, properties= None)
|
||||
await m.dispatch_msg(msg)
|
||||
assert on_connect.is_set()
|
||||
|
||||
finally:
|
||||
await m.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ignore_unknown_func(config_mqtt_conn):
|
||||
'''don't dispatch for unknwon function names'''
|
||||
|
||||
24
app/tests/test_server.py
Normal file
24
app/tests/test_server.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# test_with_pytest.py
|
||||
import pytest
|
||||
import logging
|
||||
import os
|
||||
from mock import patch
|
||||
from server import get_log_level
|
||||
|
||||
def test_get_log_level():
|
||||
|
||||
with patch.dict(os.environ, {'LOG_LVL': ''}):
|
||||
log_lvl = get_log_level()
|
||||
assert log_lvl == logging.INFO
|
||||
|
||||
with patch.dict(os.environ, {'LOG_LVL': 'DEBUG'}):
|
||||
log_lvl = get_log_level()
|
||||
assert log_lvl == logging.DEBUG
|
||||
|
||||
with patch.dict(os.environ, {'LOG_LVL': 'WARN'}):
|
||||
log_lvl = get_log_level()
|
||||
assert log_lvl == logging.WARNING
|
||||
|
||||
with patch.dict(os.environ, {'LOG_LVL': 'UNKNOWN'}):
|
||||
log_lvl = get_log_level()
|
||||
assert log_lvl == logging.INFO
|
||||
Reference in New Issue
Block a user