|
19 | 19 | SIGENERGY_ACTIVE_MODE_CHARGE, |
20 | 20 | SIGENERGY_ACTIVE_MODE_DISCHARGE, |
21 | 21 | SIGENERGY_ACTIVE_MODE_SELF, |
| 22 | + SIGENERGY_ACTIVE_MODE_SELF_GRID, |
22 | 23 | SIGENERGY_CODE_IN_OTHER_VPP, |
23 | 24 | SIGENERGY_CODE_SYSTEM_PENDING_REVIEW, |
24 | 25 | SIGENERGY_LOG_REDACT_KEYS, |
@@ -1289,6 +1290,69 @@ async def mock_send_battery_command(sid, active_mode, duration_min, charging_pow |
1289 | 1290 | return failed |
1290 | 1291 |
|
1291 | 1292 |
|
| 1293 | +def test_sigenergy_apply_controls_freeze_export_no_window(my_predbat): |
| 1294 | + """Test apply_controls sends Self-Consumption Grid for a freeze export left by execute.py.""" |
| 1295 | + failed = False |
| 1296 | + |
| 1297 | + def run_case(charge_rate, export_rate): |
| 1298 | + """Apply controls with the given rates and no active window, returning the battery commands.""" |
| 1299 | + api = MockSigenergyAPI() |
| 1300 | + system_id = "SIG001" |
| 1301 | + api.systems[system_id] = {"systemName": "Home", "batteryCapacity": 10.0} |
| 1302 | + api.devices[system_id] = [{"deviceType": "Battery", "attrMap": {"ratedChargePower": 3.0}}] |
| 1303 | + api.energy_flow[system_id] = {"batterySoc": 80.0} |
| 1304 | + # This is the control state execute.py actually leaves behind for a Freeze Export on |
| 1305 | + # SIGCLOUD: adjust_force_export(False) has already cleared the export window, so the |
| 1306 | + # only remaining signal is adjust_charge_rate(0) against a reset export rate. |
| 1307 | + api.controls[system_id] = { |
| 1308 | + "charge": {"enable": False, "start_time": "00:00:00", "end_time": "00:00:00", "target_soc": 100, "rate": charge_rate}, |
| 1309 | + "export": {"enable": False, "start_time": "00:00:00", "end_time": "00:00:00", "target_soc": 0, "rate": export_rate}, |
| 1310 | + "reserve": 10, |
| 1311 | + } |
| 1312 | + |
| 1313 | + commands_sent = [] |
| 1314 | + |
| 1315 | + async def mock_send_battery_command(sid, active_mode, duration_min, charging_power_kw=None, **kwargs): |
| 1316 | + commands_sent.append(("battery_cmd", sid, active_mode, duration_min, charging_power_kw)) |
| 1317 | + return True |
| 1318 | + |
| 1319 | + api.send_battery_command = mock_send_battery_command |
| 1320 | + |
| 1321 | + ok = run_async(api.apply_controls(system_id)) |
| 1322 | + assert ok is True, "apply_controls freeze export returned True" |
| 1323 | + return [c for c in commands_sent if c[0] == "battery_cmd"] |
| 1324 | + |
| 1325 | + # Charge rate pinned to zero with the export rate still live — a freeze export |
| 1326 | + bat_cmds = run_case(0, 3000) |
| 1327 | + assert len(bat_cmds) >= 1, "send_battery_command called for freeze export" |
| 1328 | + assert bat_cmds[0][2] == SIGENERGY_ACTIVE_MODE_SELF_GRID, "selfConsumption-grid sent for freeze export, got {}".format(bat_cmds[0][2]) |
| 1329 | + # chargingPower is bidirectional on this API, so a freeze export must not cap it — that |
| 1330 | + # would also stop the battery serving the house, which is a freeze charge not a freeze export |
| 1331 | + assert bat_cmds[0][4] is None, "no charging power cap sent for freeze export, got {}".format(bat_cmds[0][4]) |
| 1332 | + |
| 1333 | + # Both rates zero — a freeze export with a car-charging or iBoost discharge hold on top. |
| 1334 | + # selfConsumption-grid would let the battery discharge to serve the house, which the hold |
| 1335 | + # forbids, so this is plain self-consumption with the bidirectional power pinned to zero |
| 1336 | + bat_cmds = run_case(0, 0) |
| 1337 | + assert len(bat_cmds) >= 1, "send_battery_command called for freeze export with a discharge hold" |
| 1338 | + assert bat_cmds[0][2] == SIGENERGY_ACTIVE_MODE_SELF, "selfConsumption sent for freeze export with a discharge hold, got {}".format(bat_cmds[0][2]) |
| 1339 | + assert bat_cmds[0][4] == 0, "charging power pinned to 0 for freeze export with a discharge hold, got {}".format(bat_cmds[0][4]) |
| 1340 | + |
| 1341 | + # A discharge hold on its own, with charging still allowed, is not a freeze and must not be |
| 1342 | + # capped — the bidirectional power cannot express "charge allowed, discharge blocked" |
| 1343 | + bat_cmds = run_case(3000, 0) |
| 1344 | + assert len(bat_cmds) >= 1, "send_battery_command called for a discharge hold alone" |
| 1345 | + assert bat_cmds[0][2] == SIGENERGY_ACTIVE_MODE_SELF, "selfConsumption sent for a discharge hold alone, got {}".format(bat_cmds[0][2]) |
| 1346 | + assert bat_cmds[0][4] is None, "no charging power cap sent for a discharge hold alone, got {}".format(bat_cmds[0][4]) |
| 1347 | + |
| 1348 | + # A normal demand period with both rates live stays in demand |
| 1349 | + bat_cmds = run_case(3000, 3000) |
| 1350 | + assert len(bat_cmds) >= 1, "send_battery_command called for demand" |
| 1351 | + assert bat_cmds[0][2] == SIGENERGY_ACTIVE_MODE_SELF, "selfConsumption sent for demand, got {}".format(bat_cmds[0][2]) |
| 1352 | + |
| 1353 | + return failed |
| 1354 | + |
| 1355 | + |
1292 | 1356 | # --------------------------------------------------------------------------- |
1293 | 1357 | # MQTT tests |
1294 | 1358 | # --------------------------------------------------------------------------- |
@@ -3154,6 +3218,7 @@ def run_sigenergy_tests(my_predbat): |
3154 | 3218 | ("apply_controls_eco_mode", test_sigenergy_apply_controls_eco_mode), |
3155 | 3219 | ("apply_controls_deduplication", test_sigenergy_apply_controls_deduplication), |
3156 | 3220 | ("apply_controls_export_mode", test_sigenergy_apply_controls_export_mode), |
| 3221 | + ("apply_controls_freeze_export_no_window", test_sigenergy_apply_controls_freeze_export_no_window), |
3157 | 3222 | ("publish_mqtt_success", test_sigenergy_publish_mqtt_success), |
3158 | 3223 | ("redact", test_sigenergy_redact), |
3159 | 3224 | ("publish_mqtt_redacts_token", test_sigenergy_publish_mqtt_redacts_token), |
|
0 commit comments