|
21 | 21 | HELPER_MIN_SOLAR_FORECAST_THRESHOLD_SUFFIX, |
22 | 22 | HELPER_NIGHT_CHARGE_AMPERAGE_SUFFIX, |
23 | 23 | HELPER_HOME_BATTERY_MIN_SOC_SUFFIX, |
| 24 | + HELPER_CAR_READY_MONDAY_SUFFIX, |
| 25 | + HELPER_CAR_READY_TUESDAY_SUFFIX, |
| 26 | + HELPER_CAR_READY_WEDNESDAY_SUFFIX, |
| 27 | + HELPER_CAR_READY_THURSDAY_SUFFIX, |
| 28 | + HELPER_CAR_READY_FRIDAY_SUFFIX, |
| 29 | + HELPER_CAR_READY_SATURDAY_SUFFIX, |
| 30 | + HELPER_CAR_READY_SUNDAY_SUFFIX, |
24 | 31 | ) |
25 | 32 | from .utils.logging_helper import EVSCLogger |
26 | 33 | from .utils import entity_helper, state_helper |
@@ -72,6 +79,7 @@ def __init__( |
72 | 79 | self._solar_forecast_threshold_entity = None |
73 | 80 | self._night_charge_amperage_entity = None |
74 | 81 | self._home_battery_min_soc_entity = None |
| 82 | + self._car_ready_entities = {} # Dict: {0: monday_entity, ..., 6: sunday_entity} |
75 | 83 |
|
76 | 84 | # Timer and state tracking |
77 | 85 | self._timer_unsub = None |
@@ -105,6 +113,25 @@ async def async_setup(self) -> None: |
105 | 113 | self.hass, HELPER_HOME_BATTERY_MIN_SOC_SUFFIX |
106 | 114 | ) |
107 | 115 |
|
| 116 | + # Discover car_ready switches for each day (v1.3.13+) |
| 117 | + days_suffixes = [ |
| 118 | + HELPER_CAR_READY_MONDAY_SUFFIX, |
| 119 | + HELPER_CAR_READY_TUESDAY_SUFFIX, |
| 120 | + HELPER_CAR_READY_WEDNESDAY_SUFFIX, |
| 121 | + HELPER_CAR_READY_THURSDAY_SUFFIX, |
| 122 | + HELPER_CAR_READY_FRIDAY_SUFFIX, |
| 123 | + HELPER_CAR_READY_SATURDAY_SUFFIX, |
| 124 | + HELPER_CAR_READY_SUNDAY_SUFFIX, |
| 125 | + ] |
| 126 | + |
| 127 | + for idx, suffix in enumerate(days_suffixes): |
| 128 | + entity = entity_helper.find_by_suffix(self.hass, suffix) |
| 129 | + if entity: |
| 130 | + self._car_ready_entities[idx] = entity |
| 131 | + self.logger.info(f"Found car_ready entity for day {idx}: {entity}") |
| 132 | + else: |
| 133 | + self.logger.warning(f"Car ready entity not found for day {idx} (suffix: {suffix})") |
| 134 | + |
108 | 135 | # Warn about missing entities (backward compatibility) |
109 | 136 | missing_entities = [] |
110 | 137 | if not self._night_charge_enabled_entity: |
@@ -391,6 +418,41 @@ async def _start_battery_charge(self, pv_forecast: float) -> None: |
391 | 418 | ev_target = self.priority_balancer.get_ev_target_for_today() |
392 | 419 | threshold = self._get_solar_threshold() |
393 | 420 |
|
| 421 | + # ========== PRE-CHECK: Home Battery SOC (v1.3.13+) ========== |
| 422 | + self.logger.info(f"{self.logger.BATTERY} Pre-check: Validating home battery SOC...") |
| 423 | + home_soc = await self.priority_balancer.get_home_current_soc() |
| 424 | + |
| 425 | + if home_soc <= home_min_soc: |
| 426 | + # Battery below threshold! |
| 427 | + self.logger.warning( |
| 428 | + f"{self.logger.ALERT} Home battery below threshold: " |
| 429 | + f"{home_soc}% <= {home_min_soc}%" |
| 430 | + ) |
| 431 | + |
| 432 | + # Check car_ready flag for today |
| 433 | + car_ready_today = self._get_car_ready_for_today() |
| 434 | + |
| 435 | + if car_ready_today: |
| 436 | + # Car needed → Fallback to GRID |
| 437 | + self.logger.warning( |
| 438 | + f"{self.logger.CAR} Car ready flag is ON for today → " |
| 439 | + f"Fallback to GRID MODE to ensure EV is ready in the morning" |
| 440 | + ) |
| 441 | + await self._start_grid_charge(pv_forecast) |
| 442 | + return |
| 443 | + else: |
| 444 | + # Car not needed → SKIP |
| 445 | + self.logger.info( |
| 446 | + f"{self.logger.CAR} Car ready flag is OFF for today → " |
| 447 | + f"Skipping night charge, will rely on tomorrow's solar surplus" |
| 448 | + ) |
| 449 | + self.logger.info("Night charge session cancelled (waiting for solar)") |
| 450 | + self.logger.separator() |
| 451 | + return |
| 452 | + |
| 453 | + self.logger.success(f"Home battery SOC check passed: {home_soc}% > {home_min_soc}%") |
| 454 | + # ========== END PRE-CHECK ========== |
| 455 | + |
394 | 456 | self.logger.info(f" Charger amperage: {amperage}A") |
395 | 457 | self.logger.info(f" EV target SOC: {ev_target}%") |
396 | 458 | self.logger.info(f" Home battery minimum SOC: {home_min_soc}%") |
@@ -585,6 +647,37 @@ def _get_home_battery_min_soc(self) -> float: |
585 | 647 | 20.0 |
586 | 648 | ) |
587 | 649 |
|
| 650 | + def _get_car_ready_for_today(self) -> bool: |
| 651 | + """ |
| 652 | + Get car_ready flag for current day (v1.3.13+). |
| 653 | +
|
| 654 | + Returns: |
| 655 | + True if car needs to be ready in the morning (use grid as fallback) |
| 656 | + False if car not needed (skip charging, wait for solar) |
| 657 | + """ |
| 658 | + # Get current day (0=Monday, 6=Sunday) |
| 659 | + current_day = datetime.now().weekday() |
| 660 | + |
| 661 | + # Get entity for today |
| 662 | + entity_id = self._car_ready_entities.get(current_day) |
| 663 | + |
| 664 | + if not entity_id: |
| 665 | + # Fallback: weekday=True, weekend=False |
| 666 | + self.logger.warning( |
| 667 | + f"Car ready entity not found for day {current_day}, " |
| 668 | + f"using default (weekday=True, weekend=False)" |
| 669 | + ) |
| 670 | + return current_day < 5 # Monday-Friday = True |
| 671 | + |
| 672 | + # Get switch state |
| 673 | + car_ready = state_helper.get_bool(self.hass, entity_id, default=True) |
| 674 | + |
| 675 | + day_names = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] |
| 676 | + day_name = day_names[current_day] |
| 677 | + self.logger.info(f"{self.logger.CALENDAR} Car ready flag for {day_name}: {car_ready}") |
| 678 | + |
| 679 | + return car_ready |
| 680 | + |
588 | 681 | def _log_configuration(self) -> None: |
589 | 682 | """Log current configuration.""" |
590 | 683 | enabled = state_helper.get_bool(self.hass, self._night_charge_enabled_entity) if self._night_charge_enabled_entity else False |
|
0 commit comments