Skip to content

CCS2 force-refresh's unconditional sleep(25) can crash-loop a Home Assistant instance on restart #1281

Description

@Chompy78
  • Hyundai / Kia Connect version: 4.26.1 (confirmed still present, unchanged, in 4.26.5 — the latest release
    as of 2026-08-17)
  • Python version: 3.14.6
  • Operating System: Alpine Linux 3.24.1 (inside the official homeassistant/home-assistant Docker image),
    Home Assistant Core 2026.8.1, via the kia_uvo custom integration (v3.10.0)

Description

_force_refresh_vehicle_state_ccs2 in KiaUvoApiEU.py contains an unconditional, blocking sleep(25):

def _force_refresh_vehicle_state_ccs2(self, token: Token, vehicle: Vehicle) -> None:
    ...
    trigger_url = self.SPA_API_URL + "vehicles/" + vehicle.id + "/ccs2/carstatus"
    self.session.get(trigger_url, headers=headers).json()
    sleep(25)
    latest_url = trigger_url + "/latest"
    response = self.session.get(latest_url, headers=headers).json()
    ...

In Home Assistant (via kia_uvo), this runs inside hass.async_add_executor_job, i.e. on a plain
ThreadPoolExecutor thread — not an asyncio task. That thread is not cancellable through Home
Assistant's normal shutdown/reload machinery: HA can only wait for it to finish or time out its shutdown
grace period and force-kill.

This call can also fire automatically, with no user action, on the very first coordinator update after
any Home Assistant restart
VehicleManager.check_and_force_update_vehicle triggers a force-refresh
whenever (now - vehicle.last_updated_at).total_seconds() > force_refresh_interval, and
last_updated_at reflects the vehicle's own last-known server timestamp, which can already be well past
that threshold at login (e.g. right after a fresh Home Assistant restart querying a car that hasn't sent
a real update in a while). With kia_uvo's own default force_refresh_interval (24h), this is easy to
trip on the very next restart after any real gap in activity.

I hit this in production: a Home Assistant instance crash-looped roughly every ~30 seconds (Docker log
signature: Found N non-daemonic threads, i.e. a forced-shutdown-timeout, not a clean exit). Each fresh
process immediately re-entered this same blocking path before the previous instance's 25s sleep thread
had necessarily finished, self-sustaining the loop. I could not capture the original in-process traceback
(rotated out of logs by the time I diagnosed this), but the mechanism above is independently reproducible
from the source alone and fully explains the observed cadence.

Two related, already-fixed issues (#1786, #1806, fixed via #1250 / released in 4.25.4) addressed the
KeyError: 'resMsg' half of this same code path — the wake-then-read pattern itself (including this
sleep(25)) is intentional and, per #1250's own PR description, was chosen specifically to avoid a
poll-loop. That tradeoff seems reasonable for a plain script, but is risky inside any framework (Home
Assistant included) that runs this on a thread it may need to cancel or wait out during a restart/reload.

What I Did

Reproduced the trigger condition by reasoning through the actual installed source
(VehicleManager.check_and_force_update_vehicle, coordinator.py's _async_update_data) rather than a
fresh crash, since the original traceback had rotated out of my logs by the time I diagnosed this:

# VehicleManager.py
if (started_at_utc - vehicle.last_updated_at).total_seconds() > force_refresh_interval:
    self.force_refresh_vehicle_state(vehicle_id)   # -> ... -> sleep(25) on an HA executor thread

Also confirmed kia_uvo's no_force_refresh_hour_start/_finish options cannot be configured to block
this trigger for all 24 hours (the boundary-check logic in kia_uvo's coordinator.py is structurally
incapable of an always-false result across both branches) — the only working mitigation is setting
force_refresh_interval very high, which just delays the exposure rather than removing it.

Suggested fixes, roughly in order of how much they change:

  1. Add a real bounded timeout to the whole force-refresh operation (wake + sleep + read), independent of
    the per-request HTTP timeout already fixed in All HTTP requests missing timeout parameter (139 calls across 10 files) #1151 — so a caller can enforce "give up after N seconds"
    around the blocking section as a whole.
  2. Replace the unconditional sleep(25) with a short poll loop against /latest (checking whether the
    snapshot has actually advanced) with a bounded max wait — the alternative fix(EU): simpler wake+sleep+read CCS2 force refresh (alternative to #1184) #1250 itself considered and
    set aside, but worth revisiting given this failure mode.
  3. At minimum, document that _force_refresh_vehicle_state_ccs2 blocks for ~25s and should never be
    called from a thread a host application might need to cancel/await during shutdown — so integrations
    like kia_uvo can guard against calling it near a restart/reload.

Workaround in place on my end: set force_refresh_interval to a very large value so the automatic
trigger doesn't fire during any realistic monitoring window. This avoids the automatic path but not a
manually-triggered "Force Refresh" that happens to land near a restart.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions