from dataclasses import replace import unittest from netplan_v4.optimizer import optimize from netplan_v4.domain import Limits from test_v4 import AT,seq,batt class BatteryRecoveryTest(unittest.TestCase): def plan(self,b,steps=None): steps=steps or seq([0.]*3+[1000.]*3,pv=[1000.]*3+[0.]*3,sell=[0.]*6) return optimize(steps,[b],at=AT,observed_peaks={'2026-10':1.},peak_prices={'2026-10':0.}) def test_reserve_recovery_preserves_real_initial_energy(self): b=batt(capacity_kwh=1.,soc_percent=5.,min_soc_percent=15.,physical_min_soc_percent=3.,recovery_allowed=True,rearm_soc_percent=17.,discharge_blocked=True,grid_charging=False,max_charge_w=1000.,max_discharge_w=1000.) p=self.plan(b);self.assertTrue(p['executable'],p) energy=.05 for x in p['points']: target=x['batteryTargetW'];energy+=target/1000/12*(.9**.5 if target>=0 else 1/(.9**.5)) self.assertAlmostEqual(100*energy,x['socEndPercent']['b'],places=5) self.assertGreaterEqual(energy,.05-1e-6) self.assertGreaterEqual(p['points'][-1]['socEndPercent']['b'],15.-1e-5) def test_below_hardware_min_not_fabricated(self): b=batt(soc_percent=1.,min_soc_percent=15.,physical_min_soc_percent=3.,recovery_allowed=True) self.assertFalse(self.plan(b)['executable']) def test_disabled_can_rearm_after_charging(self): b=batt(capacity_kwh=10.,soc_percent=16.,min_soc_percent=15.,rearm_soc_percent=17.,discharge_blocked=True,grid_charging=False,max_charge_w=1000.,max_discharge_w=1000.) p=self.plan(b);self.assertTrue(p['executable'],p) self.assertAlmostEqual(min(0.,p['points'][0]['batteryTargetW']),0.) self.assertTrue(any(x['batteryTargetW']<-1. for x in p['points'][3:])) first=next(i for i,x in enumerate(p['points']) if x['batteryTargetW']<-1.) self.assertGreaterEqual(p['points'][first-1]['socEndPercent']['b'],17.-1e-5) def test_cannot_discharge_below_rearm_while_blocked(self): b=batt(capacity_kwh=10.,soc_percent=16.,min_soc_percent=15.,rearm_soc_percent=17.,discharge_blocked=True,grid_charging=False) p=self.plan(b,seq([1000.]*6));self.assertTrue(p['executable']) self.assertTrue(all(x['batteryTargetW']>=-1e-5 for x in p['points'])) def test_unavailable_power_does_not_get_invented(self): b=batt(capacity_kwh=1.,soc_percent=5.,min_soc_percent=15.,physical_min_soc_percent=3.,recovery_allowed=True,max_charge_w=0.,max_discharge_w=0.) p=self.plan(b);self.assertFalse(p['executable']);self.assertEqual(p['status'],'no_feasible_plan') def test_invalid_hysteresis_range_rejected(self): b=batt(rearm_soc_percent=95.) self.assertFalse(self.plan(b)['executable']) def test_no_recovery_permission_keeps_strict_behavior(self): self.assertFalse(self.plan(batt(soc_percent=5.))['executable']) if __name__=='__main__':unittest.main()