This commit is contained in:
1 parent
9e0230731b
commit
e7905c667d
10 files changed
+224
-27
No files matched your search
+81
-1
@@ -14,7 +14,7 @@ from unittest.mock import Mock, patch
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from solar_relay.config import InverterConfig, UploadConfig, date_bound, load_config
|
||||
from solar_relay.fronius import FroniusClient, powerflow_readings
|
||||
from solar_relay.fronius import FroniusClient, boiler_threshold, powerflow_readings
|
||||
from solar_relay.http import JsonHttp, TransportError
|
||||
from solar_relay.storage import Store
|
||||
from solar_relay.upload import Uploader, UploadError
|
||||
@@ -36,10 +36,12 @@ METER = api({"0": {
|
||||
"EnergyReal_WAC_Sum_Consumed": 50000, "EnergyReal_WAC_Sum_Produced": 80000,
|
||||
}})
|
||||
BOILER = {"pin1": {"state": True, "reason": "PSTR_EMRS_REASON_3"}}
|
||||
BOILER_CONFIG = {"rules": {"pin1": {"mode": "pgrid", "thresholdOn": -2000, "thresholdOff": -500}}}
|
||||
RESPONSES = {
|
||||
"/solar_api/v1/GetPowerFlowRealtimeData.fcgi": POWER,
|
||||
"/solar_api/v1/GetMeterRealtimeData.cgi": METER,
|
||||
"/api/status/emrs": BOILER,
|
||||
"/api/config/emrs": BOILER_CONFIG,
|
||||
}
|
||||
|
||||
|
||||
@@ -138,6 +140,8 @@ class NormalizationTests(unittest.TestCase):
|
||||
self.assertEqual(readings["grid_export_w"], 0)
|
||||
self.assertEqual(readings["grid_import_total_wh"], 50000)
|
||||
self.assertTrue(readings["boiler_enabled"])
|
||||
self.assertEqual(readings["boiler_threshold_w"], 2000)
|
||||
self.assertEqual(readings["boiler_threshold_source"], "api")
|
||||
self.assertEqual(result["errors"], {})
|
||||
self.assertEqual(result["raw"]["powerflow"], POWER)
|
||||
self.assertEqual(result["source_timestamps"]["powerflow"], "2026-11-01T12:00:00.000Z")
|
||||
@@ -167,6 +171,27 @@ class NormalizationTests(unittest.TestCase):
|
||||
self.assertIs(result["readings"]["boiler_enabled"], False)
|
||||
self.assertNotIn("boiler", result["errors"])
|
||||
|
||||
def test_configured_threshold_is_used_when_settings_require_authentication(self):
|
||||
responses = deepcopy(RESPONSES)
|
||||
responses["/api/config/emrs"] = TransportError("HTTP 401")
|
||||
result = FroniusClient(InverterConfig(min_request_interval_seconds=0, boiler_threshold_w=2500), http=FakeHttp(responses)).collect("test")
|
||||
self.assertTrue(result["readings"]["boiler_enabled"])
|
||||
self.assertEqual(result["readings"]["boiler_threshold_w"], 2500)
|
||||
self.assertEqual(result["readings"]["boiler_threshold_source"], "config")
|
||||
self.assertEqual(result["errors"], {})
|
||||
|
||||
def test_settings_are_matched_to_the_configured_pin_and_bad_values_use_fallback(self):
|
||||
for value in (None, 0, True, "2000", float("nan"), 100_000_000):
|
||||
with self.subTest(value=value):
|
||||
responses = deepcopy(RESPONSES)
|
||||
responses["/api/config/emrs"]["rules"]["pin1"]["thresholdOn"] = value
|
||||
result = FroniusClient(InverterConfig(min_request_interval_seconds=0), http=FakeHttp(responses)).collect("test")
|
||||
self.assertEqual(result["readings"]["boiler_threshold_w"], 2000)
|
||||
self.assertEqual(result["readings"]["boiler_threshold_source"], "config")
|
||||
settings = {"rules": {"pin1": {"mode": "pgrid", "thresholdOn": -1500},
|
||||
"pin2": {"mode": "ppv", "thresholdOn": 3200}}}
|
||||
self.assertEqual(boiler_threshold(settings, "pin2"), 3200)
|
||||
|
||||
def test_solar_api_application_error_does_not_accept_old_values(self):
|
||||
responses = deepcopy(RESPONSES)
|
||||
responses["/solar_api/v1/GetPowerFlowRealtimeData.fcgi"]["Head"]["Status"]["Code"] = 12
|
||||
@@ -265,6 +290,53 @@ class DeliveryTests(unittest.TestCase):
|
||||
uploader.upload_once()
|
||||
self.assertEqual(self.store.status()["pending"], 1)
|
||||
|
||||
def test_solar_json_result_envelope_acknowledges_the_samples(self):
|
||||
with server() as (httpd, base):
|
||||
reading = sample()
|
||||
self.store.add(reading)
|
||||
httpd.ack_override = {"result": "success", "data": {"accepted_ids": [reading["id"]]},
|
||||
"desc_lang_id": "", "desc_lang_params": []}
|
||||
uploader = Uploader(UploadConfig(url=base, token="test-token"), self.store)
|
||||
self.assertEqual(uploader.upload_once(), 1)
|
||||
self.assertEqual(self.store.status()["pending"], 0)
|
||||
|
||||
def test_failed_or_malformed_solar_result_never_clears_the_queue(self):
|
||||
with server() as (httpd, base):
|
||||
reading = sample()
|
||||
self.store.add(reading)
|
||||
uploader = Uploader(UploadConfig(url=base, token="test-token"), self.store)
|
||||
accepted = {"accepted_ids": [reading["id"]]}
|
||||
for response in [
|
||||
{"result": "error", "data": accepted},
|
||||
{"result": "error", **accepted},
|
||||
{"result": True, "data": accepted},
|
||||
{"result": "success", "data": None},
|
||||
{"result": "success", "data": []},
|
||||
{"result": "success", "data": {}, **accepted},
|
||||
]:
|
||||
with self.subTest(response=response):
|
||||
httpd.ack_override = response
|
||||
with self.assertRaises(UploadError):
|
||||
uploader.upload_once()
|
||||
self.assertEqual(self.store.status()["pending"], 1)
|
||||
|
||||
def test_legacy_samples_receive_config_fallback_without_replacing_recorded_thresholds(self):
|
||||
legacy = sample()
|
||||
del legacy["readings"]["boiler_threshold_w"]
|
||||
del legacy["readings"]["boiler_threshold_source"]
|
||||
current = sample()
|
||||
current["readings"]["boiler_threshold_w"] = 3100
|
||||
self.store.add(legacy)
|
||||
self.store.add(current)
|
||||
with server() as (httpd, base):
|
||||
uploader = Uploader(UploadConfig(url=base, token="test-token"), self.store, boiler_threshold_w=2500)
|
||||
self.assertEqual(uploader.upload_once(), 2)
|
||||
delivered = {s["id"]: s for s in httpd.requests[0]["samples"]}
|
||||
self.assertEqual(delivered[legacy["id"]]["readings"]["boiler_threshold_w"], 2500)
|
||||
self.assertEqual(delivered[legacy["id"]]["readings"]["boiler_threshold_source"], "config")
|
||||
self.assertEqual(delivered[current["id"]]["readings"]["boiler_threshold_w"], 3100)
|
||||
self.assertEqual(list(self.store.records()), [legacy, current])
|
||||
|
||||
def test_partial_ack_only_marks_the_accepted_sample(self):
|
||||
with server() as (httpd, base):
|
||||
first, second = sample(), sample()
|
||||
@@ -376,6 +448,14 @@ class ConfigTests(unittest.TestCase):
|
||||
self.assertEqual(config.collector.poll_interval_seconds, 300)
|
||||
self.assertEqual(config.collector.retention_days, 30)
|
||||
self.assertEqual(config.upload.url, "")
|
||||
self.assertEqual(config.inverter.boiler_threshold_w, 2000)
|
||||
|
||||
def test_boiler_threshold_fallback_is_configurable_and_must_be_positive_watts(self):
|
||||
config, _ = self.load('\n[inverter]\nboiler_threshold_w=2750\n')
|
||||
self.assertEqual(config.inverter.boiler_threshold_w, 2750)
|
||||
for value in ('0', '-100', 'true', '"2000"', 'nan', 'inf', '100000000'):
|
||||
with self.subTest(value=value), self.assertRaises(ValueError):
|
||||
self.load(f'\n[inverter]\nboiler_threshold_w={value}\n')
|
||||
|
||||
def test_remote_upload_requires_https(self):
|
||||
with self.assertRaisesRegex(ValueError, "HTTPS"):
|
||||
|
||||
Reference in new issue
Block a user