Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion src/mqtt_gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
14 changes: 12 additions & 2 deletions src/vehicle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down
28 changes: 28 additions & 0 deletions tests/test_vehicle_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading