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:
Stefan Allius
2024-12-08 13:25:04 +01:00
committed by GitHub
parent ac7b02bde9
commit b335881500
22 changed files with 942 additions and 624 deletions

View File

@@ -39,18 +39,18 @@ schema:
# type: str
# manufacturer: str
# daher diese variante
pv1_manufacturer: str?
pv1_type: str?
pv2_manufacturer: str?
pv2_type: str?
pv3_manufacturer: str?
pv3_type: str?
pv4_manufacturer: str?
pv4_type: str?
pv5_manufacturer: str?
pv5_type: str?
pv6_manufacturer: str?
pv6_type: str?
pv1.manufacturer: str?
pv1.type: str?
pv2.manufacturer: str?
pv2.type: str?
pv3.manufacturer: str?
pv3.type: str?
pv4.manufacturer: str?
pv4.type: str?
pv5.manufacturer: str?
pv5.type: str?
pv6.manufacturer: str?
pv6.type: str?
tsun.enabled: bool
solarman.enabled: bool
inverters.allow_all: bool
@@ -92,10 +92,10 @@ options:
# - string: PV2
# type: SF-M18/144550
# manufacturer: Shinefar
pv1_manufacturer: Shinefar
pv1_type: SF-M18/144550
pv2_manufacturer: Shinefar
pv2_type: SF-M18/144550
pv1.manufacturer: Shinefar
pv1.type: SF-M18/144550
pv2.manufacturer: Shinefar
pv2.type: SF-M18/144550
tsun.enabled: true # set default
solarman.enabled: true # set default
inverters.allow_all: false # set default

View File

@@ -1,115 +0,0 @@
import json
import os
# Dieses file übernimmt die Add-On Konfiguration und schreibt sie in die
# Konfigurationsdatei des tsun-proxy
# Die Addon Konfiguration wird in der Datei /data/options.json bereitgestellt
# Die Konfiguration wird in der Datei /home/proxy/config/config.toml
# gespeichert
# Übernehme die Umgebungsvariablen
# alternativ kann auch auf die homeassistant supervisor API zugegriffen werden
def create_config():
data = {}
data['mqtt.host'] = os.getenv('MQTT_HOST', "mqtt")
data['mqtt.port'] = os.getenv('MQTT_PORT', 1883)
data['mqtt.user'] = os.getenv('MQTT_USER', "")
data['mqtt.passwd'] = os.getenv('MQTT_PASSWORD', "")
# Lese die Add-On Konfiguration aus der Datei /data/options.json
# with open('data/options.json') as json_file:
with open('/data/options.json') as json_file:
try:
options_data = json.load(json_file)
data.update(options_data)
except json.JSONDecodeError:
pass
# Schreibe die Add-On Konfiguration in die Datei /home/proxy/config/config.toml # noqa: E501
# with open('./config/config.toml', 'w+') as f:
with open('/home/proxy/config/config.toml', 'w+') as f:
f.write(f"""
mqtt.host = '{data.get('mqtt.host')}' # URL or IP address of the mqtt broker
mqtt.port = {data.get('mqtt.port')}
mqtt.user = '{data.get('mqtt.user')}'
mqtt.passwd = '{data.get('mqtt.passwd')}'
ha.auto_conf_prefix = '{data.get('ha.auto_conf_prefix', 'homeassistant')}' # MQTT prefix for subscribing for homeassistant status updates
ha.discovery_prefix = '{data.get('ha.discovery_prefix', 'homeassistant')}' # MQTT prefix for discovery topic
ha.entity_prefix = '{data.get('ha.entity_prefix', 'tsun')}' # MQTT topic prefix for publishing inverter values
ha.proxy_node_id = '{data.get('ha.proxy_node_id', 'proxy')}' # MQTT node id, for the proxy_node_id
ha.proxy_unique_id = '{data.get('ha.proxy_unique_id', 'P170000000000001')}' # MQTT unique id, to identify a proxy instance
tsun.enabled = {str(data.get('tsun.enabled', True)).lower()}
tsun.host = '{data.get('tsun.host', 'logger.talent-monitoring.com')}'
tsun.port = {data.get('tsun.port', 5005)}
solarman.enabled = {str(data.get('solarman.enabled', True)).lower()}
solarman.host = '{data.get('solarman.host', 'iot.talent-monitoring.com')}'
solarman.port = {data.get('solarman.port', 10000)}
inverters.allow_all = {str(data.get('inverters.allow_all', False)).lower()}
""") # noqa: E501
if 'inverters' in data:
for inverter in data['inverters']:
f.write(f"""
[inverters."{inverter['serial']}"]
node_id = '{inverter['node_id']}'
suggested_area = '{inverter['suggested_area']}'
modbus_polling = {str(inverter['modbus_polling']).lower()}
# check if inverter has monitor_sn key. if not, skip monitor_sn
{f"monitor_sn = '{inverter['monitor_sn']}'" if 'monitor_sn' in inverter else ''}
# check if inverter has 'pv1_type' and 'pv1_manufacturer' keys. if not, skip pv1
{f"pv1 = {{type = '{inverter['pv1_type']}', manufacturer = '{inverter['pv1_manufacturer']}'}}" if 'pv1_type' in inverter and 'pv1_manufacturer' in inverter else ''}
# check if inverter has 'pv2_type' and 'pv2_manufacturer' keys. if not, skip pv2
{f"pv2 = {{type = '{inverter['pv2_type']}', manufacturer = '{inverter['pv2_manufacturer']}'}}" if 'pv2_type' in inverter and 'pv2_manufacturer' in inverter else ''}
# check if inverter has 'pv3_type' and 'pv3_manufacturer' keys. if not, skip pv3
{f"pv3 = {{type = '{inverter['pv3_type']}', manufacturer = '{inverter['pv3_manufacturer']}'}}" if 'pv3_type' in inverter and 'pv3_manufacturer' in inverter else ''}
# check if inverter has 'pv4_type' and 'pv4_manufacturer' keys. if not, skip pv4
{f"pv4 = {{type = '{inverter['pv4_type']}', manufacturer = '{inverter['pv4_manufacturer']}'}}" if 'pv4_type' in inverter and 'pv4_manufacturer' in inverter else ''}
# check if inverter has 'pv5_type' and 'pv5_manufacturer' keys. if not, skip pv5
{f"pv5 = {{type = '{inverter['pv5_type']}', manufacturer = '{inverter['pv5_manufacturer']}'}}" if 'pv5_type' in inverter and 'pv5_manufacturer' in inverter else ''}
# check if inverter has 'pv6_type' and 'pv6_manufacturer' keys. if not, skip pv6
{f"pv6 = {{type = '{inverter['pv6_type']}', manufacturer = '{inverter['pv6_manufacturer']}'}}" if 'pv6_type' in inverter and 'pv6_manufacturer' in inverter else ''}
""") # noqa: E501
# add filters
f.write("""
[gen3plus.at_acl]
# filter for received commands from the internet
tsun.allow = [""")
if 'gen3plus.at_acl.tsun.allow' in data:
for rule in data['gen3plus.at_acl.tsun.allow']:
f.write(f"'{rule}',")
f.write("]\ntsun.block = [")
if 'gen3plus.at_acl.tsun.block' in data:
for rule in data['gen3plus.at_acl.tsun.block']:
f.write(f"'{rule}',")
f.write("""]
# filter for received commands from the MQTT broker
mqtt.allow = [""")
if 'gen3plus.at_acl.mqtt.allow' in data:
for rule in data['gen3plus.at_acl.mqtt.allow']:
f.write(f"'{rule}',")
f.write("]\nmqtt.block = [")
if 'gen3plus.at_acl.mqtt.block' in data:
for rule in data['gen3plus.at_acl.mqtt.block']:
f.write(f"'{rule}',")
f.write("]")
if __name__ == "__main__": # pragma: no cover
create_config()

View File

@@ -27,12 +27,9 @@ cd /home || exit
mkdir -p proxy/log
mkdir -p proxy/config
echo "Create config.toml..."
python3 create_config_toml.py
cd /home/proxy || exit
export VERSION=$(cat /proxy-version.txt)
echo "Start Proxyserver..."
python3 server.py
python3 server.py --json_config=/data/options.json

View File

@@ -1,194 +0,0 @@
# test_with_pytest.py
import pytest
import tomllib
from mock import patch
from cnf.config import Config
from home.create_config_toml import create_config
from test_config import ConfigComplete, ConfigMinimum
class FakeBuffer:
rd = bytearray()
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 read(self):
return self.buf.rd
class FakeConfigFile(FakeFile):
def write(self, data: str):
self.buf.wr += data
@pytest.fixture
def patch_open():
def new_open(file: str, OpenTextMode="r"):
if file == '/data/options.json':
return FakeOptionsFile()
elif file == '/home/proxy/config/config.toml':
# write_buffer += 'bla1'.encode('utf-8')
return FakeConfigFile()
raise TimeoutError
with patch('builtins.open', new_open) as conn:
yield conn
@pytest.fixture
def ConfigTomlEmpty():
return {
'gen3plus': {'at_acl': {'mqtt': {'allow': [], 'block': []},
'tsun': {'allow': [], 'block': []}}},
'ha': {'auto_conf_prefix': 'homeassistant',
'discovery_prefix': 'homeassistant',
'entity_prefix': 'tsun',
'proxy_node_id': 'proxy',
'proxy_unique_id': 'P170000000000001'},
'inverters': {
'allow_all': False
},
'mqtt': {'host': 'mqtt', 'passwd': '', 'port': 1883, 'user': ''},
'solarman': {
'enabled': True,
'host': 'iot.talent-monitoring.com',
'port': 10000,
},
'tsun': {
'enabled': True,
'host': 'logger.talent-monitoring.com',
'port': 5005,
},
}
def test_no_config(patch_open, ConfigTomlEmpty):
_ = patch_open
test_buffer.wr = ""
test_buffer.rd = "" # empty buffer, no json
create_config()
cnf = tomllib.loads(test_buffer.wr)
assert cnf == ConfigTomlEmpty
def test_empty_config(patch_open, ConfigTomlEmpty):
_ = patch_open
test_buffer.wr = ""
test_buffer.rd = "{}" # empty json
create_config()
cnf = tomllib.loads(test_buffer.wr)
assert cnf == ConfigTomlEmpty
def test_full_config(patch_open, ConfigComplete):
_ = patch_open
test_buffer.wr = ""
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"
},
{
"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"
}
],
"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"
]
}
"""
create_config()
cnf = tomllib.loads(test_buffer.wr)
validated = Config.conf_schema.validate(cnf)
assert validated == ConfigComplete
def test_minimum_config(patch_open, ConfigMinimum):
_ = patch_open
test_buffer.wr = ""
test_buffer.rd = """
{
"inverters": [
{
"serial": "R170000000000001",
"monitor_sn": 0,
"node_id": "",
"suggested_area": "",
"modbus_polling": true,
"client_mode_host": "InverterIP",
"client_mode_port": 1234
}
],
"tsun.enabled": true,
"solarman.enabled": true,
"inverters.allow_all": true,
"gen3plus.at_acl.tsun.allow": [
"AT+Z",
"AT+UPURL",
"AT+SUPDATE"
],
"gen3plus.at_acl.mqtt.allow": [
"AT+"
]
}
"""
create_config()
cnf = tomllib.loads(test_buffer.wr)
validated = Config.conf_schema.validate(cnf)
assert validated == ConfigMinimum