diff --git a/README.md b/README.md index 23b64d7..b12f440 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ The following are the known available endpoints: |-----------------------|----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | --ha-discovery | HA_DISCOVERY_ENABLED | Home Assistant auto-discovery is enabled (True) by default. It can be disabled (False) with this parameter. | | --ha-discovery-prefix | HA_DISCOVERY_PREFIX | The default MQTT prefix for Home Assistant auto-discovery is 'homeassistant'. Another prefix can be configured with this parameter | -| --ha-show-unavailable | HA_SHOW_UNAVAILABLE | Show entities as Unavailable in Home Assistant when car polling fails. Enabled (True) by default. Can be disabled, to retain the pre 0.6.x behaviour, but do that at your own risk. | +| --ha-show-unavailable | HA_SHOW_UNAVAILABLE | Show entities as Unavailable in Home Assistant after 3 consecutive failed polls (a single failed poll is tolerated). Enabled (True) by default. Can be disabled, to retain the pre 0.6.x behaviour, but do that at your own risk. | ### A Better Route Planner (ABRP) integration diff --git a/src/mqtt_gateway.py b/src/mqtt_gateway.py index f955e93..40b8f5b 100644 --- a/src/mqtt_gateway.py +++ b/src/mqtt_gateway.py @@ -316,7 +316,7 @@ async def __refresh_vehicle_list(self) -> None: for vin in known_vins - api_vins: LOG.warning("Vehicle %s no longer in API vehicle list, stopping", vin) vh = self.vehicle_handlers.pop(vin) - vh.vehicle_state.mark_failed_refresh() + vh.vehicle_state.mark_failed_refresh(force_offline=True) task = self.__cancel_vehicle_task(vin) if task is not None: with contextlib.suppress(asyncio.CancelledError): diff --git a/src/vehicle.py b/src/vehicle.py index 3c73742..afa1276 100644 --- a/src/vehicle.py +++ b/src/vehicle.py @@ -44,6 +44,8 @@ DEFAULT_AC_TEMP = 22 PRESSURE_TO_BAR_FACTOR = 0.04 +# Consecutive failed polls before the vehicle is marked offline +VEHICLE_FAILURES_BEFORE_OFFLINE = 3 # Consecutive charge data failures before charging entities are marked unavailable CHARGE_DATA_FAILURES_BEFORE_UNAVAILABLE = 3 @@ -115,6 +117,7 @@ def __init__( ) self.__last_failed_refresh: datetime.datetime | None = None self.__failed_refresh_counter = 0 + self.__consecutive_failed_refreshes = 0 self.__failed_charge_data_counter = 0 self.__charge_data_available: bool | None = None self.__refresh_period_error = 30 @@ -476,6 +479,7 @@ def __should_do_periodic_refresh(self) -> bool: def mark_successful_refresh(self) -> None: self.last_successful_refresh = datetime.datetime.now(tz=datetime.UTC) self.last_failed_refresh = None + self.__consecutive_failed_refreshes = 0 self.publisher.publish_str(self.get_topic(mqtt_topics.AVAILABLE), "online") def mark_charge_data_received(self) -> None: @@ -496,9 +500,15 @@ def __publish_charge_data_available(self, *, available: bool) -> None: "online" if available else "offline", ) - def mark_failed_refresh(self) -> None: + def mark_failed_refresh(self, *, force_offline: bool = False) -> None: self.last_failed_refresh = datetime.datetime.now(tz=datetime.UTC) - self.publisher.publish_str(self.get_topic(mqtt_topics.AVAILABLE), "offline") + self.__consecutive_failed_refreshes += 1 + # Tolerate a few intermittent failures before flipping entities to unavailable + if ( + force_offline + or self.__consecutive_failed_refreshes >= VEHICLE_FAILURES_BEFORE_OFFLINE + ): + self.publisher.publish_str(self.get_topic(mqtt_topics.AVAILABLE), "offline") @property def refresh_period_error(self) -> int: diff --git a/tests/test_vehicle_state.py b/tests/test_vehicle_state.py index b92f0a2..2cc8983 100644 --- a/tests/test_vehicle_state.py +++ b/tests/test_vehicle_state.py @@ -55,6 +55,34 @@ def _charge_data_availability(self) -> str | None: topic = f"/vehicles/{VIN}/{mqtt_topics.DRIVETRAIN_CHARGING_DATA_AVAILABLE}" return self.publisher.map.get(topic) + def _vehicle_availability(self) -> str | None: + return self.publisher.map.get(f"/vehicles/{VIN}/{mqtt_topics.AVAILABLE}") + + def test_vehicle_offline_only_after_repeated_failures(self) -> None: + self.vehicle_state.mark_successful_refresh() + assert self._vehicle_availability() == "online" + + self.vehicle_state.mark_failed_refresh() + self.vehicle_state.mark_failed_refresh() + assert self._vehicle_availability() == "online" + + self.vehicle_state.mark_failed_refresh() + assert self._vehicle_availability() == "offline" + + def test_successful_refresh_resets_vehicle_failure_count(self) -> None: + self.vehicle_state.mark_successful_refresh() + self.vehicle_state.mark_failed_refresh() + self.vehicle_state.mark_failed_refresh() + self.vehicle_state.mark_successful_refresh() + self.vehicle_state.mark_failed_refresh() + self.vehicle_state.mark_failed_refresh() + assert self._vehicle_availability() == "online" + + def test_force_offline_is_immediate(self) -> None: + self.vehicle_state.mark_successful_refresh() + self.vehicle_state.mark_failed_refresh(force_offline=True) + assert self._vehicle_availability() == "offline" + def test_charge_data_unavailable_only_after_repeated_failures(self) -> None: self.vehicle_state.mark_charge_data_received() assert self._charge_data_availability() == "online"