Skip to content

feat: add VehicleProfile and capability properties for EU API - #1157

Draft
blka wants to merge 2 commits into
Hyundai-Kia-Connect:masterfrom
blka:feature/eu-vehicle-profile-timezone
Draft

feat: add VehicleProfile and capability properties for EU API#1157
blka wants to merge 2 commits into
Hyundai-Kia-Connect:masterfrom
blka:feature/eu-vehicle-profile-timezone

Conversation

@blka

@blka blka commented May 26, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add VehicleProfile dataclass (~70 fields) mapping the EU /api/v1/spa/vehicles/{id}/profile endpoint response (basic, device, option, serviceOption, batteryType, detailInfo, dtcCategory sections + top-level appMode dispatch field)
  • Add 29 capability properties on Vehicle that read from Vehicle.profile for HA getattr compatibility — return None when profile is unavailable or the specific option flag is absent (unknown), bool when present
  • Add str_or_none utility for int-or-string option-value coercion (API returns some option fields as ints, others as strings; capability props compare against string values)
  • Add supports_vehicle_profile flag on ApiImpl (False by default), set True only on KiaUvoApiEU
  • _fetch_vehicle_profiles + _map_vehicle_profile in ApiImplType1, piggybacked onto the existing vehicle-list call inside get_vehicles (login only, not per poll cycle), gated on supports_vehicle_profile. Uses self.session.get (ApiImplSession, consistent with feat: add ApiImplSession with timeout and connection pooling #1160)
  • Graceful degradation: a profile fetch failure for a vehicle leaves vehicle.profile = None — no crash, no extra API calls on subsequent polls
  • Non-EU regions (CA, USA, BR) make zero extra calls. AU/IN/CN stay off until their /profile schema is verified
  • Unit tests + EU profile fixture (EV6, CCS2/ccNC, appMode=EV5)

Profile request frequency

The /profile GET is 1 request per vehicle, only at integration startup (first poll, when vehicle_manager.vehicles is empty → initialize_vehiclesget_vehicles), on HA restart, config-entry reload, or re-auth. It is not called on the poll cycle (update_all_vehicles_with_cached_state), on force-refresh, or on token refresh (EU refresh_vehicles is a no-op). So a 1-vehicle account makes exactly 1 extra GET at startup.

Capability properties (29)

From vehicle options/status (13):
steering_wheel_heater_supported, side_mirror_heater_supported, rear_window_heater_supported, front_window_heating_supported, sunroof_supported, digital_key_supported, air_purifier_supported, remote_heat_control_supported, ignition_control_supported, horn_light_supported, light_only_supported, ev_alarm_supported, is_left_hand_drive

From option/serviceOption flags (16, added from volunteer /profile dumps in kia_uvo discussion #1764 — Kia EV6 CCS2/ccNC + 2021 e-Niro CCSP):
svm_supported (Surround View Monitor / 360°), v2l_supported (Vehicle-to-Load), v2x_supported (Vehicle-to-Everything), v2g_supported (Vehicle-to-Grid), frunk_supported, climate_control_supported (clmtCtrl), battery_preconditioning_supported, digital_side_mirror_supported (cms / Camera Monitoring System), wireless_charging_supported, remote_front_window_heat_supported, window_safety_supported, charge_port_door_supported, charging_current_control_supported, v2l_soc_set_supported (V2L discharge SoC limit), window_control_supported, remote_light_control_supported

These flags are None when the profile or the specific option field is absent (some fields are reported only on certain vehicle generations — e.g. V2GOption/wirelessChargingOption/windowControlOption on older CCSP, V2LOption/frunkOption/batteryPreconditioningOption on newer ccNC), so consumers can distinguish "unknown" from "not supported".

Use cases (capability flags enable future kia_uvo gating)

  1. Horn/hazard buttons (bug-fix): kia_uvo shows start_hazard_lights / start_hazard_lights_and_horn unconditionally; CA/USA raise NotImplementedErrorUnsupportedControlError at action time. horn_light_supported lets kia_uvo hide the buttons where the endpoint does not exist.
  2. Sunroof for EU/AU (missing feature): kia_uvo does not populate sunroof_is_open for EU/AU today; sunroof_supported + status population fixes this.
  3. Capability gating instead of None-gating (stability): today kia_uvo gates ~all entities on getattr(vehicle, attr, None) is not None, which proxies "data present in last poll", not "vehicle supports". Entities flap after partial polls / force-refresh when a value is momentarily None. A stable flag from /profile stops the flapping.
  4. Conditional climate options: CCS2 climate-start hardcodes strgWhlHeating / sideRearMirrorHeating / windshieldFrontDefogState unconditionally; capability flags let the library send them only when supported.
  5. EV-feature gating: battery_preconditioning_supported / v2l_supported / charge_port_door_supported / charging_current_control_supported enable kia_uvo to expose or hide EV-specific entities per vehicle instead of guessing from data presence.

Per @Lekensteyn's concern on #1158: profile is fetched inside get_vehicles (piggybacked on the existing vehicle-list call at login), not per poll cycle, and only when supports_vehicle_profile is True. UserAccount (#1158) is dropped — no consumer, #1158 closed.

Architecture

Shared SPA logic lives in ApiImplType1 (precedent: ccs2, climate, drvSeatLoc). The EU mapping is consolidated into Type1 with a supports_vehicle_profile flag (precedent: supports_window_control), so AU/IN/CN can adopt it by flipping the flag once their /profile schema is verified — no code duplication. Coexists with supports_valet_mode from #1207. appMode is exposed as VehicleProfile.app_mode for the scheduled-charge/climate endpoint dispatch (flat /ccs2/reservation/{charge,hvac} for EV5 vs combined /chargehvac for older vehicles — follow-up scheduled-charging PR).

Test plan

  • pytest tests/ — 434 passed, 11 skipped (10 snapshots)
  • ruff check . + ruff format --check — clean
  • pre-commit run --all-files — all hooks pass incl. mypy
  • Live API test against EU Hyundai /profile endpoint (identifiers redacted) — profile populated, 29 capability properties return coherent values
  • Re-verified /profile schema stability (2026-07-24): recursive key-diff vs fixture — 0 schema drift; appMode and the 16 option flags confirmed present on EV6 (ccs2/ccNC), absent on 2021 e-Niro (CCSP)
  • Graceful degradation verified — profile fetch failure leaves vehicle.profile = None without crashing

Deferred to separate PRs

  • EU country timezone detection (data_timezone per-vehicle)
  • HA integration changes (new entities, capability gating)
  • Hardening the original 13 capability properties to the same None-when-field-absent semantics used by the 16 new ones (cross-protocol consistency)

@Lekensteyn

Copy link
Copy Markdown
Contributor

Same comment as the one from #1158 applies, what is the use case for this? Let's not add extra API calls if there are no benefits to end users.

@blka
blka force-pushed the feature/eu-vehicle-profile-timezone branch from d9c97e5 to fccd015 Compare June 22, 2026 11:34
@blka

blka commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @Lekensteyn — restructured to address the use-case question and the extra-call concern.

Use cases the capability flags enable (kia_uvo consumer planned for a near-future follow-up PR):

  1. Horn/hazard buttons (bug-fix): kia_uvo currently shows start_hazard_lights / start_hazard_lights_and_horn unconditionally (exists_fn=lambda _: True). CA/USA raise NotImplementedError → the user clicks and gets UnsupportedControlError at action time. horn_light_supported lets kia_uvo hide the buttons where the endpoint does not exist.
  2. Sunroof for EU/AU (missing feature): kia_uvo does not populate sunroof_is_open for EU/AU/HyundaiUSA today (region omission) → users with a sunroof never see the entity. sunroof_supported + status population fixes this.
  3. Capability gating instead of None-gating (stability): today kia_uvo gates ~all entities on getattr(vehicle, attr, None) is not None — this proxies "data present in last poll", not "vehicle supports". Entities flap after partial polls / force-refresh when a value is momentarily None. A stable flag from /profile stops the flapping.
  4. Conditional climate options: today CCS2 climate-start hardcodes strgWhlHeating / sideRearMirrorHeating / windshieldFrontDefogState in the payload unconditionally; capability flags let the library send them only when supported.

Extra API call: profile is now fetched inside ApiImplType1.get_vehicles() (piggybacked on the existing vehicle-list call at login), not per poll cycle, and only when supports_vehicle_profile is True. Non-supporting regions (CA, USA, BR) make zero extra calls. EU is the only region with the flag set (live-verified); AU/IN/CN stay off until their /profile endpoint is verified, so they make zero extra calls too.

Surface reduction: UserAccount (PR #1158) is dropped — no consumer, #1158 closed. The public fetch_vehicle_profiles method + base no-op stub are removed; the fetch is an internal detail of get_vehicles. Net smaller diff.

Architecture: shared SPA logic lives in ApiImplType1 (precedent: ccs2, climate, drvSeatLoc); the EU mapping is consolidated up into Type1 with a supports_vehicle_profile flag (precedent: supports_window_control), so AU/IN/CN can adopt it by flipping the flag once their /profile schema is verified — no code duplication.

Live-verified against the EU API (Hyundai, EU account; identifiers redacted):

supports_vehicle_profile: True
get_vehicles returned 1 vehicle — profile populated automatically inside get_vehicles
brand=H country=pl drvSeatLoc=L hvacTempType=1
raw option fields: heatingSteeringWheel=1 sunroof=1 horn=1 lightOnly=1 digitalKey2=3 evAlarm=0
capability properties:
  steering_wheel_heater_supported: True
  side_mirror_heater_supported: True
  rear_window_heater_supported: True
  front_window_heating_supported: False
  sunroof_supported: True
  digital_key_supported: True
  air_purifier_supported: False
  remote_heat_control_supported: True
  ignition_control_supported: False
  horn_light_supported: True
  light_only_supported: True
  ev_alarm_supported: False
  is_left_hand_drive: True   (drvSeatLoc=L, PL = LHD — consistent with #1192)

All 13 capability properties return coherent values; profile fetch worked end-to-end inside get_vehicles.

Happy to adjust if the capability set (13) is too broad — a fallback would be to keep only the ones with an immediate use case (horn, sunroof, heaters, defrost, lamps) and defer digital_key/air_purifier/ev_alarm.

@blka
blka force-pushed the feature/eu-vehicle-profile-timezone branch from fccd015 to 5930c06 Compare July 1, 2026 11:45
@blka

blka commented Jul 1, 2026

Copy link
Copy Markdown
Contributor Author

@Lekensteyn can you check pls?

@blka
blka force-pushed the feature/eu-vehicle-profile-timezone branch from 5930c06 to a472f62 Compare July 1, 2026 12:01
@blka
blka force-pushed the feature/eu-vehicle-profile-timezone branch 2 times, most recently from 9af9c7a to 6b68153 Compare July 24, 2026 13:53
- VehicleProfile dataclass (~50 fields) mapping EU /api/v1/spa/vehicles/{id}/profile
- 13 capability properties on Vehicle reading from Vehicle.profile (HA getattr pattern, None when no profile)
- str_or_none utility for int-or-string option coercion
- supports_vehicle_profile flag on ApiImpl (False by default), True on KiaUvoApiEU
- _fetch_vehicle_profiles + _map_vehicle_profile in ApiImplType1, piggybacked on get_vehicles (login only, not per poll)
- Graceful degradation: profile fetch failure leaves vehicle.profile = None
- 21 unit tests + EU profile fixture

Non-supporting regions (CA, USA, BR) make zero extra calls. AU/IN/CN stay off until their /profile schema is verified.
@blka
blka force-pushed the feature/eu-vehicle-profile-timezone branch from 6b68153 to 31e49fa Compare July 24, 2026 14:34
@blka

blka commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

@cdnninja — could you take a look when you have a moment?

This is scoped as a read-only foundation: VehicleProfile + capability flags mapped from /profile, fetched once per vehicle at startup (inside get_vehicles, gated on supports_vehicle_profile), with no per-poll or per-force-refresh cost. It is meant as a gate for the follow-up features — capability-based entity gating in kia_uvo, scheduled-charge/climate endpoint dispatch via app_mode, and the MQTT work — so they can build on a merged base rather than a moving fork.

Kept it focused (no consumers wired here on purpose). Happy to adjust scope or naming.

@cdnninja cdnninja left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initial thoughts.

Comment thread hyundai_kia_connect_api/ApiImplType1.py Outdated
if profile_data:
vehicle.profile = self._map_vehicle_profile(profile_data[0])
except Exception:
_LOGGER.debug(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should plan for this to work, if widespread failures occur something is wrong and we should take care of it. Maybe up this to warning?

navi_applied: bool | None = None
web_manual_url: str | None = None

# option (raw API values)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we know what these are? It isn't clear from the title for some of them. I would think many of these should be bool. They will need a way to be normalized for all regions.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a similar thought the logic for supported looks to focus on a value so seems these are known in some way but hard codes vehicle to a region.

Per PR Hyundai-Kia-Connect#1157 review: profile fetch failures should be visible at
warning level since widespread failures indicate a real problem worth
investigating. Add exc_info=True so the traceback is captured.

The vehicle still degrades gracefully (profile stays None, capability
properties return None) — only the log level changes.
@blka
blka marked this pull request as draft July 27, 2026 11:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants