Feature negative dc charging - #3795
Conversation
There was a problem hiding this comment.
Pull request overview
This PR extends the charging/control logic to support bidirectional operation (negative currents/power), including counter/loadmanagement tracking for “export” limits and updated algorithm behavior/tests for DC discharge scenarios.
Changes:
- Allow/control negative setpoints for bidirectional (dis)charging across templates and EV current calculation.
- Add “exported” headroom tracking on counters (power + currents) and use it in loadmanagement limiting.
- Update BIDI algorithm flow and expand integration test coverage for instant discharging / DC current limits.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/helpermodules/setdata.py | Relaxes validation for chargepoint /set/required_power to allow negative values. |
| packages/control/loadmanagement.py | Adds BIDI-specific current availability method and extends current/power limiting to handle negative currents. |
| packages/control/ev/ev.py | Passes bidirectional state into instant-charging template logic. |
| packages/control/ev/charge_template.py | Enables instant-charging discharge behavior (negative current) gated by BidiState. |
| packages/control/ev/charge_template_test.py | Updates unit test to match new instant-charging signature (adds BidiState). |
| packages/control/counter.py | Tracks remaining “exported” power/currents headroom and updates raw-value accounting for discharge scenarios. |
| packages/control/algorithm/integration_test/bidi_charging_test.py | Expands integration tests for BIDI instant discharge (DC current, counter export limits, multi-CP behavior). |
| packages/control/algorithm/common.py | Allows negative diffs (discharge) and updates current monotonicity logic for discharge. |
| packages/control/algorithm/chargemodes.py | Reorders/extends mode tuples and slice definitions to include additional BIDI discharge consideration. |
| packages/control/algorithm/bidi_charging.py | Refactors BIDI handling to use loadmanagement-based limiting and adds instant-discharge handling based on DC current. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| try: | ||
| # Am meisten belastete Phase trägt am meisten zur Leistungsreduktion bei. | ||
| currents[i] = available_currents[i] / sum(available_currents) * raw_power_left / cp_voltage | ||
| currents[i] = available_currents[i] / sum(available_currents) * power_left / cp_voltage |
| self._validate_value(msg, float, [(float("-inf"), 0), (6, 32), (0, 0)]) | ||
| elif "/set/required_power" in msg.topic: | ||
| self._validate_value(msg, float, [(0, float("inf"))]) | ||
| self._validate_value(msg, float) |
| if cp.data.set.charging_ev_data.data.get.soc is None: | ||
| raise ValueError(f"LP{cp.num}: Auto-Bat SoC unbekannt, daher keine Entladung möglich.") | ||
| if cp.data.set.charging_ev_data.data.get.soc > 0: | ||
| # Auto-bat ist nicht leer |
There was a problem hiding this comment.
| if cp.data.set.charging_ev_data.data.get.soc is None: | |
| raise ValueError(f"LP{cp.num}: Auto-Bat SoC unbekannt, daher keine Entladung möglich.") | |
| if cp.data.set.charging_ev_data.data.get.soc > 0: | |
| # Auto-bat ist nicht leer |
Ob das Auto leer ist oder aus anderen Gründen die Entladung verweigert (fast leer, Temperatur, etc.) wird ja dadurch abgefangen, dass nur die tatsächlich entladenen Leistung zum Laden von anderen Fahrzeugen verwendet wird. Dann kann diese Restriktion entfallen.
| counters = data.data.counter_all_data.get_counters_to_check(cp.num) | ||
| for counter in counters: |
There was a problem hiding this comment.
Gibt es einen Grund, dass Du nicht die Methode mode_and_counter_generator verwendest, Die auch min_current,.. verwenden, um über die Zähler zu iterieren?
There was a problem hiding this comment.
Wenn ich mode_and_counter_generator verwende, schlägt der Test test_cp3_cp4_bidi_discharge fehl.
In dem Test wird geprüft, ob bei mehreren Bidi-CPs (im Zielladen) alle gleichmäßig entladen werden und nicht nur einer und die anderen nicht.
-> Deswegen habe ich das so gelöst. Ich brauche eine Liste mit allen CPs, die sich in diesem Mode befinden, um das Entladen entsprechend aufteilen zu können.
| dc_current = cp.data.set.charging_ev_data.charge_template.data.chargemode.instant_charging.dc_current | ||
| if dc_current < 0: | ||
| # Phasen in use berücksichtigen | ||
| missing_currents = [dc_current for i in range(0, cp.data.get.phases_in_use)] | ||
| missing_currents += [0] * (3 - len(missing_currents)) | ||
|
|
||
| for index in range(0, 3): | ||
| missing_currents[index] = cp.check_min_max_current( | ||
| missing_currents[index], cp.data.get.phases_in_use) |
There was a problem hiding this comment.
Müsste das nicht control_parameter.required_currents entsprechen?
There was a problem hiding this comment.
Ja das ist genau der Punkt, denn wir schonmal besprochen haben:
Durch das self.surplus_controlled.set_required_current_to_max()in calc_current() steht im control_parameter.required_currents immer der Maximalwert.
-> Wir brauchen hier aber den im UI eingestellten Wert.
| if min(element_current) < 0: | ||
| # nur Hausverbraucher ohne Einspeisung | ||
| currents_exported_raw = list(map(operator.sub, currents_exported_raw, element_current)) | ||
| continue |
There was a problem hiding this comment.
Müsste hier nicht ein else hin? Die entladenden Fahrzeuge sollen nicht aus dem Zähler herausgerechnet werden, sondern die entladene Leistung als verfügbar auf ladende Fahrzeuge verteilt werden.
Fürs Debug in _chargepoint.py folgende Zeilen setzen:
pub_to_broker("openWB/set/chargepoint/" + str(self.num) + "/get/max_charge_power", 500000)
pub_to_broker("openWB/set/chargepoint/" + str(self.num) + "/get/max_discharge_power", -500000)
pub_to_broker("openWB/set/chargepoint/" + str(self.num) + "/get/evse_signaling", "HLC")