@@ -167,6 +167,10 @@ def __init__(self, hass: HomeAssistant, config: Dict[str, Any]) -> None:
167167 # Phase 8: Consumption/solar predictor (#3)
168168 self ._predictor = ConsumptionPredictor ()
169169
170+ # Per-cycle caches (initialized here, populated in _async_update_data)
171+ self ._cycle_forecast = None
172+ self ._cycle_vehicle_soc : Optional [float ] = None
173+
170174 # EV stall detection for self-healing
171175 self ._ev_stalled_since : Optional [float ] = None
172176
@@ -313,6 +317,19 @@ async def _async_update_data(self) -> Dict[str, Any]:
313317 self ._observer_mode = observer_state .state == "on"
314318
315319 try :
320+ # Per-cycle caches — avoid redundant lookups within one 10s cycle (#52)
321+ self ._cycle_forecast = self ._forecast_reader .read_forecast ()
322+ # Cache vehicle SOC (read in both _async_update_data and _determine_charging_strategy)
323+ _vehicle_soc_entity = self .config .get ("vehicle_soc_entity" , "" )
324+ self ._cycle_vehicle_soc = None
325+ if _vehicle_soc_entity :
326+ _soc_state = self .hass .states .get (_vehicle_soc_entity )
327+ if _soc_state and _soc_state .state not in (STATE_UNKNOWN , STATE_UNAVAILABLE ):
328+ try :
329+ self ._cycle_vehicle_soc = float (_soc_state .state )
330+ except (ValueError , TypeError ):
331+ pass
332+
316333 # Step 1: Read power values from sensors
317334 power = self ._sensor_reader .read_power ()
318335
@@ -505,15 +522,9 @@ async def _async_update_data(self) -> Dict[str, Any]:
505522 solar = lifetime .get ("total_solar_kwh" , 0 )
506523 result ["lifetime_ev_solar_share" ] = round (solar / total * 100 , 1 ) if total > 0 else 0
507524
508- # Vehicle SOC (if configured)
509- vehicle_soc_entity = self .config .get ("vehicle_soc_entity" , "" )
510- if vehicle_soc_entity :
511- soc_state = self .hass .states .get (vehicle_soc_entity )
512- if soc_state and soc_state .state not in (STATE_UNKNOWN , STATE_UNAVAILABLE ):
513- try :
514- result ["vehicle_soc" ] = float (soc_state .state )
515- except (ValueError , TypeError ):
516- pass
525+ # Vehicle SOC (from per-cycle cache)
526+ if self ._cycle_vehicle_soc is not None :
527+ result ["vehicle_soc" ] = self ._cycle_vehicle_soc
517528
518529 # EV departure time (if configured via input_datetime entity)
519530 departure_entity = self .config .get ("ev_departure_time_entity" , "" )
@@ -561,7 +572,7 @@ async def _update_analytics_phases(
561572 # Forecast (Phase 0.3)
562573 forecast_data = ForecastSensorData ()
563574 try :
564- forecast = self ._forecast_reader . read_forecast ()
575+ forecast = self ._cycle_forecast
565576 if forecast .available :
566577 forecast_data .forecast_today_kwh = forecast .forecast_today_kwh
567578 forecast_data .forecast_tomorrow_kwh = forecast .forecast_tomorrow_kwh
@@ -877,14 +888,7 @@ def _determine_charging_strategy(self, power: PowerReadings, energy: Any) -> tup
877888 ev_battery_capacity = self .config .get ("ev_battery_capacity_kwh" , 40 )
878889 ev_target_soc = self .config .get ("ev_target_soc" , 80 )
879890
880- vehicle_soc = None
881- if vehicle_soc_entity :
882- soc_state = self .hass .states .get (vehicle_soc_entity )
883- if soc_state and soc_state .state not in (STATE_UNKNOWN , STATE_UNAVAILABLE ):
884- try :
885- vehicle_soc = float (soc_state .state )
886- except (ValueError , TypeError ):
887- pass
891+ vehicle_soc = self ._cycle_vehicle_soc
888892
889893 if vehicle_soc is not None :
890894 # SOC-based: remaining = (target_soc - current_soc) / 100 * capacity
@@ -965,7 +969,7 @@ def _determine_charging_strategy(self, power: PowerReadings, energy: Any) -> tup
965969 if power .battery_soc >= buffer_soc :
966970 # Use forecast if available to check if surplus alone is enough
967971 try :
968- forecast = self ._forecast_reader . read_forecast ()
972+ forecast = self ._cycle_forecast
969973 if forecast .available :
970974 surplus_factor = 0.5
971975 estimated_surplus = forecast .forecast_remaining_today_kwh * surplus_factor
@@ -999,7 +1003,7 @@ def _determine_charging_strategy(self, power: PowerReadings, energy: Any) -> tup
9991003 )
10001004 reason = f"Zone 2: SOC={ power .battery_soc :.0f} % in [{ priority_soc } %..{ buffer_soc } %) — surplus only"
10011005 try :
1002- forecast = self ._forecast_reader . read_forecast ()
1006+ forecast = self ._cycle_forecast
10031007 if forecast .available :
10041008 surplus_factor = 0.5
10051009 estimated_surplus = forecast .forecast_remaining_today_kwh * surplus_factor
@@ -1055,7 +1059,7 @@ def _build_charging_context(
10551059 # Use EV budget (with battery redirect) instead of surplus-style available_power
10561060 forecast_remaining = 0
10571061 try :
1058- forecast = self ._forecast_reader . read_forecast ()
1062+ forecast = self ._cycle_forecast
10591063 if forecast .available :
10601064 forecast_remaining = forecast .forecast_remaining_today_kwh
10611065 except Exception :
0 commit comments