@@ -563,6 +563,13 @@ def autorepeat_bms_charge(datadict: dict[str, Any], battery_capacity: float, max
563563 except Exception :
564564 f = 1.0
565565
566+ # Near full charge rate limit
567+ chargeable_soc = max (0 , max_charge_soc - battery_capacity )
568+ if chargeable_soc <= 4 :
569+ # For last few % of charge, further reduce the rate limit to account
570+ # for non-ideal charging curves and reduce battery wear
571+ f = f * (float (chargeable_soc + 2.0 ) / 6.0 )
572+
566573 # BMS charge capability approximation
567574 bms_a = datadict .get ("bms_charge_max_current" , None )
568575 batt_v = (
@@ -631,19 +638,92 @@ def autorepeat_function_powercontrolmode8_recompute(initval: int, descr: Any, da
631638
632639 if power_control == "Mode 8 - PV and BAT control - Duration" :
633640 pvlimit = setpvlimit # import capping is done later
634- elif power_control == "Negative Injection Price" : # grid export zero; PV restricted to house_load and battery charge
635- datadict .get ("measured_power" , 0 ) # positive for export, negative for import - for future correction purposes
636- houseload = max (0 , houseload )
637- if battery_capacity >= 92 :
638- pvlimit = houseload + abs (setpvlimit ) * (100.0 - battery_capacity ) / 15.0 + 60 # slow down charging - nearly full
639- else :
640- pvlimit = setpvlimit + houseload + 60 # inverter overhead 40
641- pvlimit = max (houseload , pvlimit )
642- pushmode_power = houseload - min (pv , pvlimit ) - 90 + pv / 14 # some kind of empiric correction for losses - machine learning would be better
641+ elif power_control == "Negative Injection Price" :
642+ # --- Negative Injection Price (Mode 8 custom) ---
643+ # Controller goals:
644+ # 1) If PV < house load (deficit): discharge the battery up to the deficit (respecting min SOC),
645+ # aiming to prefer a slight grid import bias over export bias.
646+ # 2) If PV ≥ house load (surplus): let PV feed the battery first. PV limit is then adjusted using
647+ # bounded step changes based on the measured power to prevent export.
648+
649+ # Use the alternative house load for house load measurement, clamping to strict positive values.
650+ hl = max (0 , int (houseload_alt ))
651+
652+ # SOC bounds
653+ min_discharge_soc = datadict .get ("selfuse_discharge_min_soc" , 10 )
654+ max_charge_soc = datadict .get ("battery_charge_upper_soc" , 100 )
655+ # bias towards import
656+ export_target = int (datadict .get ("negative_injection_bias" , - 20 ) or - 20 )
657+ export_deadband_w = int (datadict .get ("export_feedback_deadband_w" , 50 ) or 50 )
658+ min_step_w = int (datadict .get ("export_first_step_min_w" , 100 ) or 100 )
659+ max_step_w = int (datadict .get ("export_feedback_max_w" , 500 ) or 500 )
660+
661+ # Local copies
662+ battery_charge = max (0 , int (datadict .get ("battery_power_charge" , 0 ) or 0 ))
663+ pvlimit = setpvlimit
664+ cur_pvlimit = max (0 , setpvlimit if (cur_pvlimit := datadict .get ("remotecontrol_current_pv_power_limit" , None )) is None else cur_pvlimit )
665+ pushmode_power = 0 # + = discharge, - = charge
666+
667+ # Debug inputs
643668 _LOGGER .debug (
644- f"***debug*** setpvlimit: { setpvlimit } pvlimit: { pvlimit } pushmode: { pushmode_power } houseload:{ houseload } pv: { pv } batcap: { battery_capacity } "
669+ f"[Mode8 Negative Injection] inputs pv={ pv } W hl={ houseload } W hl_alt={ houseload_alt } W (using hl) imp_lim={ import_limit } W "
670+ f"soc={ battery_capacity } % min_soc={ min_discharge_soc } % max_soc={ max_charge_soc } % cur_pvlimit={ cur_pvlimit } W "
671+ f"battery_charge={ battery_charge } W"
645672 )
646673
674+ # Optional probes (if available)
675+ measured_power = datadict .get ("measured_power" , None )
676+ _LOGGER .debug (f"[Mode8 Negative Injection] probes: measured_power={ measured_power if measured_power is not None else 'n/a' } " )
677+
678+ if pv >= hl or cur_pvlimit < setpvlimit :
679+ # Surplus or limited pv path: battery is requested to charge at up to the rate
680+ # limit from PV alone then use measured export as the control signal to adjust PV limit.
681+ # Below target: PV should be reduced to prevent export.
682+ # At/above target: PV can be increased to reduce import in bounded steps.
683+ # If PV has been limited below the setpoint and is now below house load, continue in this
684+ # loop to release PV restriction slowly.
685+ surplus = max (0 , pv - hl )
686+ measured_power = int (measured_power or 0 )
687+ error = measured_power - export_target
688+ control_state = "surplus" if pv >= hl else "clipping"
689+
690+ # Battery gets surplus up to BMS limit
691+ desired_charge , bms_cap_w , pct_cap_w = autorepeat_bms_charge (datadict , battery_capacity , max_charge_soc , surplus )
692+ pushmode_power = - desired_charge
693+
694+ if abs (error ) <= export_deadband_w :
695+ step_w = 0
696+ pvlimit = cur_pvlimit
697+ control_reason = "hold"
698+ elif error > 0 :
699+ step_w = min (max_step_w , max (min_step_w , error ))
700+ pvlimit = max (0 , cur_pvlimit - step_w )
701+ control_reason = "decrease-pv"
702+ else :
703+ step_w = min (max_step_w , max (min_step_w , - error ))
704+ pvlimit = min (setpvlimit , cur_pvlimit + step_w )
705+ control_reason = "increase-pv"
706+
707+ _LOGGER .debug (
708+ f"[Mode8 Negative Injection] { control_state } : surplus={ surplus } W measured_power={ measured_power } W "
709+ f"export_target={ export_target } W error={ error } W step={ step_w } W reason={ control_reason } "
710+ f"bms_cap≈{ bms_cap_w } W pct_cap={ pct_cap_w } W -> charge={ desired_charge } W pvlimit={ pvlimit } W hl={ hl } W"
711+ )
712+
713+ else :
714+ # Deficit path: discharge battery up to the current house deficit (if SOC allows).
715+ # Note this is only reached if pvlimit has been restored to above the house load by
716+ # the limited pv path and we therefore have insufficient PV to cover the load.
717+ deficit = hl + export_target - pv
718+ if battery_capacity > min_discharge_soc :
719+ pushmode_power = min (deficit , 30000 )
720+ else :
721+ pushmode_power = 0
722+ _LOGGER .debug (
723+ f"[Mode8 Negative Injection] deficit: deficit={ deficit } W export_target={ export_target } W "
724+ f"soc={ battery_capacity } % chosen_push={ pushmode_power } W"
725+ )
726+
647727 elif power_control == "Negative Injection and Consumption Price" : # disable PV, charge from grid
648728 pvlimit = 0
649729 pushmode_power = houseload - import_limit
@@ -720,14 +800,10 @@ def autorepeat_function_powercontrolmode8_recompute(initval: int, descr: Any, da
720800
721801 # Export limit no readscale:
722802 export_limit = datadict .get ("export_control_user_limit" , 30000 )
723- inverter_limit = datadict .get ("inverter_power_type" , 30000 )
724-
725- # Both house load and exported power come from the inverter and must fit within the inverter
726- # power limit. For example if we have a 6kW inverter and 4kW of house load, then we cannot
727- # export more than 2kW even if our export limit is higher. Trim the export limit so that any
728- # excess can be used for charging the battery rather than PV being clamped.
729- export_available = max (inverter_limit - hl , 0 )
730- export_limit = min (export_limit , export_available )
803+
804+ # Test mode: do not trim the export target by inverter power minus house load.
805+ # This lets Export-First try to use the configured export limit directly before charging the battery.
806+ export_available = export_limit
731807
732808 # SOC bounds
733809 min_discharge_soc = datadict .get ("selfuse_discharge_min_soc" , 10 )
0 commit comments