@@ -140,6 +140,7 @@ async def device_switch(hass: HomeAssistant, dev: dict, on: bool) -> None:
140140
141141
142142_UNSET = object ()
143+ URGENT_DEPARTURE_H = 2
143144
144145
145146class PilotCoordinator (DataUpdateCoordinator ):
@@ -191,10 +192,39 @@ def _changed(event: Event) -> None:
191192
192193 async def async_setup (self ) -> None :
193194 await self .store .async_load ()
195+ self ._restore_runtime_state ()
194196 await self .calibrator .async_load ()
195197 await self ._backfill_runtime ()
196198 await self ._seed_price_archive ()
197199
200+ def _restore_runtime_state (self ) -> None :
201+ """Log, recent readings and switch countdowns from before a restart.
202+ Readings older than the car's averaging window are dropped, so a long
203+ outage still starts fresh."""
204+ st = self .store .data
205+ self .log .extend (e for e in st ["log" ][:LOG_LENGTH ] if isinstance (e , dict ))
206+ now = dt_util .now ()
207+ keep = timedelta (minutes = CAR_AVERAGE_MINUTES + 2 )
208+ for row in st ["samples" ]:
209+ try :
210+ ts = dt_util .parse_datetime (row [0 ])
211+ vals = tuple (float (x ) for x in row [1 :4 ])
212+ except (TypeError , ValueError , IndexError ):
213+ continue
214+ if ts is not None and timedelta (0 ) <= now - ts <= keep :
215+ self .samples .append ((ts , * vals ))
216+ ids = {d [CONF_DEV_ID ] for d in self .devices }
217+ for did , (want , since ) in st ["pending" ].items ():
218+ ts = dt_util .parse_datetime (since ) if isinstance (since , str ) else None
219+ if did in ids and ts is not None and now - ts <= timedelta (hours = 1 ):
220+ self ._pending [did ] = (bool (want ), ts )
221+
222+ def _persist_runtime_state (self ) -> None :
223+ st = self .store .data
224+ st ["samples" ] = [[s [0 ].isoformat (), round (s [1 ], 3 ), round (s [2 ], 3 ), round (s [3 ], 3 )] for s in self .samples ]
225+ st ["pending" ] = {did : [want , ts .isoformat ()] for did , (want , ts ) in self ._pending .items ()}
226+ st ["log" ] = list (self .log )
227+
198228 async def _seed_price_archive (self ) -> None :
199229 """Fill the price archive from the recorder's hourly statistics of a
200230 current-price sensor, so the cheap threshold works from day one."""
@@ -295,6 +325,8 @@ def _query():
295325
296326 def _log (self , text : str ) -> None :
297327 self .log .appendleft ({"zeit" : dt_util .now ().strftime ("%d.%m. %H:%M" ), "text" : text })
328+ self .store .data ["log" ] = list (self .log )
329+ self .store .save ()
298330
299331 @property
300332 def mode (self ) -> str :
@@ -452,10 +484,14 @@ async def _async_update_data(self) -> dict:
452484 plugged = None
453485 if self .car_cfg :
454486 c = self .car_cfg
455- car_kw = power_kw (self .hass , c .get (CONF_CAR_POWER_SENSOR )) or 0.0
487+ meas = power_kw (self .hass , c .get (CONF_CAR_POWER_SENSOR ))
456488 ch = is_on (self .hass , c .get (CONF_CAR_CHARGING_SENSOR ))
457- charging = bool (ch ) if ch is not None else (car_kw > 0.3 or self .car .switch_on ())
458- if c .get (CONF_CAR_POWER_SENSOR ) is None and charging and c .get (CONF_CAR_CURRENT_ENTITY ):
489+ charging = bool (ch ) if ch is not None else ((meas or 0.0 ) > 0.3 or self .car .switch_on ())
490+ car_kw = meas if meas is not None else 0.0
491+ # no power reading (none configured, or e.g. an MQTT sensor still
492+ # empty after a restart): estimate it from the set current, so the
493+ # car's draw doesn't end up in the base load
494+ if meas is None and charging and c .get (CONF_CAR_CURRENT_ENTITY ):
459495 car_kw = (number (self .hass , c [CONF_CAR_CURRENT_ENTITY ]) or 0 ) * self .car .kw_per_a
460496 plugged = plugged_state (self .hass , c .get (CONF_CAR_PLUGGED_SENSOR ))
461497 if plugged is None :
@@ -558,7 +594,9 @@ def inputs(pv_kw: float, base_kw: float, car_fixed: float | None) -> P.Inputs:
558594 if forced or replan or slot != self ._car_slot :
559595 win = [s for s in self .samples if (now - s [0 ]).total_seconds () <= CAR_AVERAGE_MINUTES * 60 ]
560596 covered = (win [- 1 ][0 ] - win [0 ][0 ]).total_seconds () / 60 if len (win ) > 1 else 0
561- if covered >= CAR_MIN_COVERAGE_MINUTES or forced or (replan and win ):
597+ # a departure within 2 h doesn't wait for 10 min of readings
598+ urgent = nd is not None and nd .start - now <= timedelta (hours = URGENT_DEPARTURE_H )
599+ if covered >= CAR_MIN_COVERAGE_MINUTES or forced or ((replan or urgent ) and win ):
562600 pv_m = sum (s [1 ] for s in win ) / len (win )
563601 base_m = statistics .median (s [2 ] for s in win )
564602 self .car_plan = P .make_plan (inputs (pv_m , base_m , None ))
@@ -591,6 +629,8 @@ def inputs(pv_kw: float, base_kw: float, car_fixed: float | None) -> P.Inputs:
591629 car_fixed = min (car5 , cp .car_must_kw )
592630 self .device_plan = P .make_plan (inputs (pv5 , base5 , car_fixed ))
593631 await self ._apply_devices (now , dev_on )
632+ self ._persist_runtime_state ()
633+ self .store .save ()
594634
595635 self .status = self ._build_status (now , pv , load , base , soc , export , car_in , car_kw , charging , sun ,
596636 load_stale , paused )
0 commit comments