initial commit

This commit is contained in:
2022-12-20 21:26:47 +01:00
commit 2962a6db69
722 changed files with 63886 additions and 0 deletions

View File

@@ -0,0 +1,121 @@
import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
Platform,
CONF_USERNAME,
CONF_REGION,
CONF_PIN,
CONF_PASSWORD,
CONF_SCAN_INTERVAL,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
import hashlib
from .const import (
DOMAIN,
CONF_BRAND,
DEFAULT_PIN,
BRANDS,
REGIONS,
CONF_FORCE_REFRESH_INTERVAL,
CONF_NO_FORCE_REFRESH_HOUR_FINISH,
CONF_NO_FORCE_REFRESH_HOUR_START,
CONF_ENABLE_GEOLOCATION_ENTITY,
CONF_USE_EMAIL_WITH_GEOCODE_API,
)
from .coordinator import HyundaiKiaConnectDataUpdateCoordinator
from .services import async_setup_services, async_unload_services
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[str] = [
Platform.BINARY_SENSOR,
Platform.SENSOR,
Platform.DEVICE_TRACKER,
Platform.LOCK,
Platform.NUMBER,
# Platform.CLIMATE,
]
async def async_setup(hass: HomeAssistant, config_entry: ConfigEntry):
return True
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up Hyundai / Kia Connect from a config entry."""
coordinator = HyundaiKiaConnectDataUpdateCoordinator(hass, config_entry)
try:
await coordinator.async_config_entry_first_refresh()
except Exception as ex:
raise ConfigEntryNotReady(f"Config Not Ready: {ex}")
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][config_entry.unique_id] = coordinator
hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)
async_setup_services(hass)
return True
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS
):
del hass.data[DOMAIN][config_entry.unique_id]
if not hass.data[DOMAIN]:
async_unload_services(hass)
return unload_ok
async def async_migrate_entry(hass, config_entry: ConfigEntry):
if config_entry.version == 1:
_LOGGER.debug(f"{DOMAIN} - config data- {config_entry}")
username = config_entry.data.get(CONF_USERNAME)
password = config_entry.data.get(CONF_PASSWORD)
pin = config_entry.data.get(CONF_PIN, DEFAULT_PIN)
region = config_entry.data.get(CONF_REGION, "")
brand = config_entry.data.get(CONF_BRAND, "")
geolocation_enable = config_entry.data.get(CONF_ENABLE_GEOLOCATION_ENTITY, "")
geolocation_use_email = config_entry.data.get(
CONF_USE_EMAIL_WITH_GEOCODE_API, ""
)
no_force_finish_hour = config_entry.data.get(
CONF_NO_FORCE_REFRESH_HOUR_FINISH, ""
)
no_force_start_hour = config_entry.data.get(
CONF_NO_FORCE_REFRESH_HOUR_START, ""
)
force_refresh_interval = config_entry.data.get(CONF_FORCE_REFRESH_INTERVAL, "")
scan_interval = config_entry.data.get(CONF_SCAN_INTERVAL, "")
title = f"{BRANDS[brand]} {REGIONS[region]} {username}"
unique_id = hashlib.sha256(title.encode("utf-8")).hexdigest()
new_data = {
CONF_USERNAME: username,
CONF_PASSWORD: password,
CONF_PIN: pin,
CONF_REGION: region,
CONF_BRAND: brand,
CONF_ENABLE_GEOLOCATION_ENTITY: geolocation_enable,
CONF_USE_EMAIL_WITH_GEOCODE_API: geolocation_use_email,
CONF_NO_FORCE_REFRESH_HOUR_FINISH: no_force_finish_hour,
CONF_NO_FORCE_REFRESH_HOUR_START: no_force_start_hour,
CONF_FORCE_REFRESH_INTERVAL: force_refresh_interval,
CONF_SCAN_INTERVAL: scan_interval,
}
registry = hass.helpers.entity_registry.async_get(hass)
entities = hass.helpers.entity_registry.async_entries_for_config_entry(
registry, config_entry.entry_id
)
for entity in entities:
registry.async_remove(entity.entity_id)
hass.config_entries.async_update_entry(
config_entry, unique_id=unique_id, title=title, data=new_data
)
config_entry.version = 2
_LOGGER.info("Migration to version %s successful", config_entry.version)
return True

View File

@@ -0,0 +1,265 @@
"""Sensor for Hyundai / Kia Connect integration."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
import logging
from typing import Final
from hyundai_kia_connect_api import Vehicle
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .coordinator import HyundaiKiaConnectDataUpdateCoordinator
from .entity import HyundaiKiaConnectEntity
_LOGGER = logging.getLogger(__name__)
@dataclass
class HyundaiKiaBinarySensorEntityDescription(BinarySensorEntityDescription):
"""A class that describes custom binary sensor entities."""
is_on: Callable[[Vehicle], bool] | None = None
on_icon: str | None = None
off_icon: str | None = None
SENSOR_DESCRIPTIONS: Final[tuple[HyundaiKiaBinarySensorEntityDescription, ...]] = (
HyundaiKiaBinarySensorEntityDescription(
key="engine_is_running",
name="Engine",
is_on=lambda vehicle: vehicle.engine_is_running,
on_icon="mdi:engine",
off_icon="mdi:engine-off",
),
HyundaiKiaBinarySensorEntityDescription(
key="defrost_is_on",
name="Defrost",
is_on=lambda vehicle: vehicle.defrost_is_on,
on_icon="mdi:car-defrost-front",
off_icon="mdi:car-defrost-front",
),
HyundaiKiaBinarySensorEntityDescription(
key="steering_wheel_heater_is_on",
name="Steering Wheel Heater",
is_on=lambda vehicle: vehicle.steering_wheel_heater_is_on,
on_icon="mdi:steering",
off_icon="mdi:steering",
),
HyundaiKiaBinarySensorEntityDescription(
key="back_window_heater_is_on",
name="Back Window Heater",
is_on=lambda vehicle: vehicle.back_window_heater_is_on,
on_icon="mdi:car-defrost-rear",
off_icon="mdi:car-defrost-rear",
),
HyundaiKiaBinarySensorEntityDescription(
key="side_mirror_heater_is_on",
name="Side Mirror Heater",
is_on=lambda vehicle: vehicle.side_mirror_heater_is_on,
on_icon="mdi:car-side",
off_icon="mdi:car-side",
),
HyundaiKiaBinarySensorEntityDescription(
key="front_left_door_is_open",
name="Front Left Door",
is_on=lambda vehicle: vehicle.front_left_door_is_open,
on_icon="mdi:car-door",
off_icon="mdi:car-door",
device_class=BinarySensorDeviceClass.DOOR,
),
HyundaiKiaBinarySensorEntityDescription(
key="front_right_door_is_open",
name="Front Right Door",
is_on=lambda vehicle: vehicle.front_right_door_is_open,
on_icon="mdi:car-door",
off_icon="mdi:car-door",
device_class=BinarySensorDeviceClass.DOOR,
),
HyundaiKiaBinarySensorEntityDescription(
key="back_left_door_is_open",
name="Back Left Door",
is_on=lambda vehicle: vehicle.back_left_door_is_open,
on_icon="mdi:car-door",
off_icon="mdi:car-door",
device_class=BinarySensorDeviceClass.DOOR,
),
HyundaiKiaBinarySensorEntityDescription(
key="back_right_door_is_open",
name="Back Right Door",
is_on=lambda vehicle: vehicle.back_right_door_is_open,
on_icon="mdi:car-door",
off_icon="mdi:car-door",
device_class=BinarySensorDeviceClass.DOOR,
),
HyundaiKiaBinarySensorEntityDescription(
key="trunk_is_open",
name="Trunk",
is_on=lambda vehicle: vehicle.trunk_is_open,
on_icon="mdi:car-back",
off_icon="mdi:car-back",
device_class=BinarySensorDeviceClass.DOOR,
),
HyundaiKiaBinarySensorEntityDescription(
key="hood_is_open",
name="Hood",
is_on=lambda vehicle: vehicle.hood_is_open,
on_icon="mdi:car",
off_icon="mdi:car",
device_class=BinarySensorDeviceClass.DOOR,
),
HyundaiKiaBinarySensorEntityDescription(
key="ev_battery_is_charging",
name="EV Battery Charge",
is_on=lambda vehicle: vehicle.ev_battery_is_charging,
device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
),
HyundaiKiaBinarySensorEntityDescription(
key="ev_battery_is_plugged_in",
name="EV Battery Plug",
is_on=lambda vehicle: vehicle.ev_battery_is_plugged_in,
device_class=BinarySensorDeviceClass.PLUG,
),
HyundaiKiaBinarySensorEntityDescription(
key="fuel_level_is_low",
name="Fuel Low Level",
is_on=lambda vehicle: vehicle.fuel_level_is_low,
on_icon="mdi:gas-station-off",
off_icon="mdi:gas-station",
),
HyundaiKiaBinarySensorEntityDescription(
key="smart_key_battery_warning_is_on",
name="Smart Key Battery Warning",
is_on=lambda vehicle: vehicle.smart_key_battery_warning_is_on,
on_icon="mdi:battery-alert",
off_icon="mdi:battery",
device_class=BinarySensorDeviceClass.BATTERY,
),
HyundaiKiaBinarySensorEntityDescription(
key="washer_fluid_warning_is_on",
name="Washer Fluid Warning",
is_on=lambda vehicle: vehicle.washer_fluid_warning_is_on,
on_icon="mdi:wiper-wash-alert",
off_icon="mdi:wiper-wash",
device_class=BinarySensorDeviceClass.PROBLEM,
),
HyundaiKiaBinarySensorEntityDescription(
key="tire_pressure_all_warning_is_on",
name="Tire Pressure - All",
is_on=lambda vehicle: vehicle.tire_pressure_all_warning_is_on,
on_icon="mdi:car-tire-alert",
off_icon="mdi:car-tire-alert",
device_class=BinarySensorDeviceClass.PROBLEM,
),
HyundaiKiaBinarySensorEntityDescription(
key="tire_pressure_rear_left_warning_is_on",
name="Tire Pressure - Rear Left",
is_on=lambda vehicle: vehicle.tire_pressure_rear_left_warning_is_on,
on_icon="mdi:car-tire-alert",
off_icon="mdi:car-tire-alert",
device_class=BinarySensorDeviceClass.PROBLEM,
),
HyundaiKiaBinarySensorEntityDescription(
key="tire_pressure_front_left_warning_is_on",
name="Tire Pressure - Front Left",
is_on=lambda vehicle: vehicle.tire_pressure_front_left_warning_is_on,
on_icon="mdi:car-tire-alert",
off_icon="mdi:car-tire-alert",
device_class=BinarySensorDeviceClass.PROBLEM,
),
HyundaiKiaBinarySensorEntityDescription(
key="tire_pressure_front_right_warning_is_on",
name="Tire Pressure - Front right",
is_on=lambda vehicle: vehicle.tire_pressure_front_right_warning_is_on,
on_icon="mdi:car-tire-alert",
off_icon="mdi:car-tire-alert",
device_class=BinarySensorDeviceClass.PROBLEM,
),
HyundaiKiaBinarySensorEntityDescription(
key="tire_pressure_rear_right_warning_is_on",
name="Tire Pressure - Rear Right",
is_on=lambda vehicle: vehicle.tire_pressure_rear_right_warning_is_on,
on_icon="mdi:car-tire-alert",
off_icon="mdi:car-tire-alert",
device_class=BinarySensorDeviceClass.PROBLEM,
),
HyundaiKiaBinarySensorEntityDescription(
key="air_control_is_on",
name="Air Conditioner",
is_on=lambda vehicle: vehicle.air_control_is_on,
on_icon="mdi:air-conditioner",
off_icon="mdi:air-conditioner",
),
HyundaiKiaBinarySensorEntityDescription(
key="ev_charge_port_door_is_open",
name="EV Charge Port",
is_on=lambda vehicle: vehicle.ev_charge_port_door_is_open,
on_icon="mdi:ev-station",
off_icon="mdi:ev-station",
device_class=BinarySensorDeviceClass.DOOR,
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up binary_sensor platform."""
coordinator = hass.data[DOMAIN][config_entry.unique_id]
entities: list[HyundaiKiaConnectBinarySensor] = []
for vehicle_id in coordinator.vehicle_manager.vehicles.keys():
vehicle: Vehicle = coordinator.vehicle_manager.vehicles[vehicle_id]
for description in SENSOR_DESCRIPTIONS:
if getattr(vehicle, description.key, None) is not None:
entities.append(
HyundaiKiaConnectBinarySensor(coordinator, description, vehicle)
)
async_add_entities(entities)
return True
class HyundaiKiaConnectBinarySensor(BinarySensorEntity, HyundaiKiaConnectEntity):
"""Hyundai / Kia Connect binary sensor class."""
def __init__(
self,
coordinator: HyundaiKiaConnectDataUpdateCoordinator,
description: HyundaiKiaBinarySensorEntityDescription,
vehicle: Vehicle,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator, vehicle)
self.entity_description: HyundaiKiaBinarySensorEntityDescription = description
self._attr_unique_id = f"{DOMAIN}_{vehicle.id}_{description.key}"
self._attr_name = f"{vehicle.name} {description.name}"
@property
def is_on(self) -> bool | None:
"""Return true if the binary sensor is on."""
if self.entity_description.is_on is not None:
return self.entity_description.is_on(self.vehicle)
return None
@property
def icon(self):
"""Return the icon to use in the frontend, if any."""
if (
self.entity_description.on_icon == self.entity_description.off_icon
) is None:
return BinarySensorEntity.icon
return (
self.entity_description.on_icon
if self.is_on
else self.entity_description.off_icon
)

View File

@@ -0,0 +1,242 @@
"""Switches for Hyundai / Kia Connect integration."""
from __future__ import annotations
import logging
from time import sleep
from hyundai_kia_connect_api import ClimateRequestOptions, Vehicle, VehicleManager
from homeassistant.components.climate import ClimateEntity, ClimateEntityDescription
from homeassistant.components.climate.const import (
CURRENT_HVAC_COOL,
CURRENT_HVAC_HEAT,
CURRENT_HVAC_IDLE,
CURRENT_HVAC_OFF,
HVAC_MODE_AUTO,
HVAC_MODE_COOL,
HVAC_MODE_HEAT,
HVAC_MODE_OFF,
SUPPORT_TARGET_TEMPERATURE,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .coordinator import HyundaiKiaConnectDataUpdateCoordinator
from .entity import HyundaiKiaConnectEntity
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up binary_sensor platform."""
coordinator = hass.data[DOMAIN][config_entry.unique_id]
for vehicle_id in coordinator.vehicle_manager.vehicles.keys():
vehicle: Vehicle = coordinator.vehicle_manager.vehicles[vehicle_id]
async_add_entities([HyundaiKiaCarClimateControlSwitch(coordinator, vehicle)])
class HyundaiKiaCarClimateControlSwitch(HyundaiKiaConnectEntity, ClimateEntity):
"""Hyundai / Kia Connect Car Climate Control."""
vehicle_manager: VehicleManager
vehicle: Vehicle
# The python lib climate request is also treated as
# internal target state that can be sent to the car
climate_config: ClimateRequestOptions
# TODO: if possible in Climate, add possibility to set those
# as well. Are there maybe additional properties?
heat_status_int_to_str: dict[int | None, str | None] = {
None: None,
0: "Off",
1: "Steering Wheel and Rear Window",
2: "Rear Window",
3: "Steering Wheel",
}
heat_status_str_to_int = {v: k for [k, v] in heat_status_int_to_str.items()}
def get_internal_heat_int_for_climate_request(self):
if (
self.vehicle.steering_wheel_heater_is_on
and self.vehicle.back_window_heater_is_on
):
return 1
elif self.vehicle.back_window_heater_is_on:
return 2
elif self.vehicle.steering_wheel_heater_is_on:
return 3
else:
return 0
def __init__(
self,
coordinator: HyundaiKiaConnectDataUpdateCoordinator,
vehicle: Vehicle,
) -> None:
"""Initialize the Climate Control."""
super().__init__(coordinator, vehicle)
self.entity_description = ClimateEntityDescription(
key="climate_control",
icon="mdi:air-conditioner",
name="Climate Control",
unit_of_measurement=vehicle._air_temperature_unit,
)
self.vehicle_manager = coordinator.vehicle_manager
self._attr_unique_id = f"{DOMAIN}_{vehicle.id}_climate_control"
self._attr_name = f"{vehicle.name} Climate Control"
# set the Climate Request to the current actual state of the car
self.climate_config = ClimateRequestOptions(
set_temp=self.vehicle.air_temperature,
climate=self.vehicle.air_control_is_on,
heating=self.get_internal_heat_int_for_climate_request(),
defrost=self.vehicle.defrost_is_on,
)
@property
def temperature_unit(self) -> str:
"""Get the Cars Climate Control Temperature Unit."""
return self.vehicle._air_temperature_unit
@property
def current_temperature(self) -> float | None:
"""Get the current in-car temperature."""
return self.vehicle.air_temperature
@property
def target_temperature(self) -> float | None:
"""Get the desired in-car target temperature."""
# TODO: use Coordinator data, not internal state
return self.climate_config.set_temp
@property
def target_temperature_step(self) -> float | None:
"""Get the step size for adjusting the in-car target temperature."""
# TODO: get from lib
return 0.5
# TODO: unknown
@property
def min_temp(self) -> float:
"""Get the minimum settable temperature."""
# TODO: get from lib
return 14
# TODO: unknown
@property
def max_temp(self) -> float:
"""Get the maximum settable temperature."""
# TODO: get from lib
return 30
@property
def hvac_mode(self) -> str:
"""Get the configured climate control operation mode."""
if not self.vehicle.air_control_is_on:
return HVAC_MODE_OFF
# Cheating: there is no perfect mapping to either heat or cool,
# as the API can only set target temp and then decides: so we
# just derive the same by temperature change direction.
if self.current_temperature > self.climate_config.set_temp:
return HVAC_MODE_COOL
if self.current_temperature < self.climate_config.set_temp:
return HVAC_MODE_HEAT
# TODO: what could be a sensible answer if target temp is reached?
return HVAC_MODE_AUTO
@property
def hvac_action(self) -> str | None:
# TODO: use Coordinator data, not internal state
"""
Get what the in-car climate control is currently doing.
Computed value based on current and desired temp and configured operation mode.
"""
if not self.vehicle.air_control_is_on:
return CURRENT_HVAC_OFF
# if temp is lower than target, it HEATs
if self.current_temperature < self.climate_config.set_temp:
return CURRENT_HVAC_HEAT
# if temp is higher than target, it COOLs
if self.current_temperature > self.climate_config.set_temp:
return CURRENT_HVAC_COOL
# target temp reached
if self.current_temperature == self.climate_config.set_temp:
return CURRENT_HVAC_IDLE
# should not happen, fallback
return CURRENT_HVAC_OFF
@property
def hvac_modes(self) -> list[str]:
"""Supported in-car climate control modes."""
return [
HVAC_MODE_OFF,
# if only heater is activated
HVAC_MODE_HEAT,
# if only AC is activated
HVAC_MODE_COOL,
]
@property
def supported_features(self) -> int:
"""Supported in-car climate control features."""
return SUPPORT_TARGET_TEMPERATURE
async def async_set_hvac_mode(self, hvac_mode):
"""Set the operation mode of the in-car climate control."""
if hvac_mode == HVAC_MODE_OFF:
await self.hass.async_add_executor_job(
self.vehicle_manager.stop_climate,
self.vehicle.id,
)
self.vehicle.air_control_is_on = False
else:
await self.hass.async_add_executor_job(
self.vehicle_manager.start_climate,
self.vehicle.id,
self.climate_config,
)
self.vehicle.air_control_is_on = True
self.coordinator.async_request_refresh()
self.async_write_ha_state()
async def async_set_temperature(self, **kwargs):
"""Set the desired in-car temperature. Does not turn on the AC."""
old_temp = self.climate_config.set_temp
self.climate_config.set_temp = kwargs.get(ATTR_TEMPERATURE)
# activation is controlled separately, but if system is turned on
# and temp has changed, send update to car
if self.hvac_mode != HVAC_MODE_OFF and old_temp != self.climate_config.set_temp:
# Car does not accept changing the temp after starting the heating. So we have to turn off first
await self.hass.async_add_executor_job(
self.vehicle_manager.stop_climate,
self.vehicle.id,
)
# Wait, because the car ignores the start_climate command if it comes too fast after stopping
# TODO: replace with some more event driven method
await self.hass.async_add_executor_job(sleep, 5.0)
await self.hass.async_add_executor_job(
self.vehicle_manager.start_climate,
self.vehicle.id,
self.climate_config,
)
self.coordinator.async_request_refresh()
self.async_write_ha_state()

View File

@@ -0,0 +1,177 @@
"""Config flow for Hyundai / Kia Connect integration."""
from __future__ import annotations
import hashlib
import logging
from typing import Any
from hyundai_kia_connect_api import Token, VehicleManager
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_PASSWORD,
CONF_PIN,
CONF_REGION,
CONF_SCAN_INTERVAL,
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.exceptions import HomeAssistantError
from .const import (
BRANDS,
CONF_BRAND,
CONF_FORCE_REFRESH_INTERVAL,
CONF_NO_FORCE_REFRESH_HOUR_FINISH,
CONF_NO_FORCE_REFRESH_HOUR_START,
DEFAULT_FORCE_REFRESH_INTERVAL,
DEFAULT_NO_FORCE_REFRESH_HOUR_FINISH,
DEFAULT_NO_FORCE_REFRESH_HOUR_START,
DEFAULT_PIN,
DEFAULT_SCAN_INTERVAL,
DOMAIN,
REGIONS,
CONF_ENABLE_GEOLOCATION_ENTITY,
CONF_USE_EMAIL_WITH_GEOCODE_API,
DEFAULT_ENABLE_GEOLOCATION_ENTITY,
DEFAULT_USE_EMAIL_WITH_GEOCODE_API,
)
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
vol.Optional(CONF_PIN, default=DEFAULT_PIN): str,
vol.Required(CONF_REGION): vol.In(REGIONS),
vol.Required(CONF_BRAND): vol.In(BRANDS),
}
)
async def validate_input(hass: HomeAssistant, user_input: dict[str, Any]) -> Token:
"""Validate the user input allows us to connect."""
api = VehicleManager.get_implementation_by_region_brand(
user_input[CONF_REGION],
user_input[CONF_BRAND],
)
token: Token = await hass.async_add_executor_job(
api.login, user_input[CONF_USERNAME], user_input[CONF_PASSWORD]
)
if token is None:
raise InvalidAuth
return token
class HyundaiKiaConnectOptionFlowHandler(config_entries.OptionsFlow):
"""Handle an option flow for Hyundai / Kia Connect."""
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize option flow instance."""
self.config_entry = config_entry
self.schema = vol.Schema(
{
vol.Required(
CONF_SCAN_INTERVAL,
default=self.config_entry.options.get(
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
),
): vol.All(vol.Coerce(int), vol.Range(min=5, max=999)),
vol.Required(
CONF_FORCE_REFRESH_INTERVAL,
default=self.config_entry.options.get(
CONF_FORCE_REFRESH_INTERVAL, DEFAULT_FORCE_REFRESH_INTERVAL
),
): vol.All(vol.Coerce(int), vol.Range(min=30, max=999)),
vol.Required(
CONF_NO_FORCE_REFRESH_HOUR_START,
default=self.config_entry.options.get(
CONF_NO_FORCE_REFRESH_HOUR_START,
DEFAULT_NO_FORCE_REFRESH_HOUR_START,
),
): vol.All(vol.Coerce(int), vol.Range(min=0, max=23)),
vol.Required(
CONF_NO_FORCE_REFRESH_HOUR_FINISH,
default=self.config_entry.options.get(
CONF_NO_FORCE_REFRESH_HOUR_FINISH,
DEFAULT_NO_FORCE_REFRESH_HOUR_FINISH,
),
): vol.All(vol.Coerce(int), vol.Range(min=0, max=23)),
vol.Optional(
CONF_ENABLE_GEOLOCATION_ENTITY,
default=self.config_entry.options.get(
CONF_ENABLE_GEOLOCATION_ENTITY,
DEFAULT_ENABLE_GEOLOCATION_ENTITY,
),
): bool,
vol.Optional(
CONF_USE_EMAIL_WITH_GEOCODE_API,
default=self.config_entry.options.get(
CONF_USE_EMAIL_WITH_GEOCODE_API,
DEFAULT_USE_EMAIL_WITH_GEOCODE_API,
),
): bool,
}
)
async def async_step_init(self, user_input=None) -> FlowResult:
"""Handle options init setup."""
if user_input is not None:
return self.async_create_entry(
title=self.config_entry.title, data=user_input
)
return self.async_show_form(step_id="init", data_schema=self.schema)
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Hyundai / Kia Connect."""
VERSION = 2
@staticmethod
@callback
def async_get_options_flow(config_entry: ConfigEntry):
"""Initiate options flow instance."""
return HyundaiKiaConnectOptionFlowHandler(config_entry)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
if user_input is None:
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
)
errors = {}
try:
await validate_input(self.hass, user_input)
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
title = f"{BRANDS[user_input[CONF_BRAND]]} {REGIONS[user_input[CONF_REGION]]} {user_input[CONF_USERNAME]}"
await self.async_set_unique_id(
hashlib.sha256(title.encode("utf-8")).hexdigest()
)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=title, data=user_input)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
class InvalidAuth(HomeAssistantError):
"""Error to indicate there is invalid auth."""

View File

@@ -0,0 +1,28 @@
"""Constants for the Hyundai / Kia Connect integration."""
DOMAIN: str = "kia_uvo"
CONF_BRAND: str = "brand"
CONF_FORCE_REFRESH_INTERVAL: str = "force_refresh"
CONF_NO_FORCE_REFRESH_HOUR_START: str = "no_force_refresh_hour_start"
CONF_NO_FORCE_REFRESH_HOUR_FINISH: str = "no_force_refresh_hour_finish"
CONF_ENABLE_GEOLOCATION_ENTITY: str = "enable_geolocation_entity"
CONF_USE_EMAIL_WITH_GEOCODE_API: str = "use_email_with_geocode_api"
REGION_EUROPE: str = "Europe"
REGION_CANADA: str = "Canada"
REGION_USA: str = "USA"
REGIONS = {1: REGION_EUROPE, 2: REGION_CANADA, 3: REGION_USA}
BRAND_KIA: str = "Kia"
BRAND_HYUNDAI: str = "Hyundai"
BRANDS = {1: BRAND_KIA, 2: BRAND_HYUNDAI}
DEFAULT_PIN: str = ""
DEFAULT_SCAN_INTERVAL: int = 30
DEFAULT_FORCE_REFRESH_INTERVAL: int = 240
DEFAULT_NO_FORCE_REFRESH_HOUR_START: int = 22
DEFAULT_NO_FORCE_REFRESH_HOUR_FINISH: int = 6
DEFAULT_ENABLE_GEOLOCATION_ENTITY: bool = False
DEFAULT_USE_EMAIL_WITH_GEOCODE_API: bool = False
DYNAMIC_UNIT: str = "dynamic_unit"

View File

@@ -0,0 +1,221 @@
"""Coordinator for Hyundai / Kia Connect integration."""
from __future__ import annotations
from datetime import timedelta
import logging
from site import venv
from hyundai_kia_connect_api import (
VehicleManager,
ClimateRequestOptions,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_PASSWORD,
CONF_PIN,
CONF_REGION,
CONF_SCAN_INTERVAL,
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util
from .const import (
CONF_BRAND,
CONF_FORCE_REFRESH_INTERVAL,
CONF_NO_FORCE_REFRESH_HOUR_FINISH,
CONF_NO_FORCE_REFRESH_HOUR_START,
DEFAULT_FORCE_REFRESH_INTERVAL,
DEFAULT_NO_FORCE_REFRESH_HOUR_FINISH,
DEFAULT_NO_FORCE_REFRESH_HOUR_START,
DEFAULT_SCAN_INTERVAL,
DOMAIN,
DEFAULT_ENABLE_GEOLOCATION_ENTITY,
DEFAULT_USE_EMAIL_WITH_GEOCODE_API,
CONF_USE_EMAIL_WITH_GEOCODE_API,
CONF_ENABLE_GEOLOCATION_ENTITY,
)
_LOGGER = logging.getLogger(__name__)
class HyundaiKiaConnectDataUpdateCoordinator(DataUpdateCoordinator):
"""Class to manage fetching data from the API."""
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Initialize."""
self.platforms: set[str] = set()
self.vehicle_manager = VehicleManager(
region=config_entry.data.get(CONF_REGION),
brand=config_entry.data.get(CONF_BRAND),
username=config_entry.data.get(CONF_USERNAME),
password=config_entry.data.get(CONF_PASSWORD),
pin=config_entry.data.get(CONF_PIN),
geocode_api_enable=config_entry.options.get(
CONF_ENABLE_GEOLOCATION_ENTITY, DEFAULT_ENABLE_GEOLOCATION_ENTITY
),
geocode_api_use_email=config_entry.options.get(
CONF_USE_EMAIL_WITH_GEOCODE_API, DEFAULT_USE_EMAIL_WITH_GEOCODE_API
),
)
self.scan_interval: int = (
config_entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) * 60
)
self.force_refresh_interval: int = (
config_entry.options.get(
CONF_FORCE_REFRESH_INTERVAL, DEFAULT_FORCE_REFRESH_INTERVAL
)
* 60
)
self.no_force_refresh_hour_start: int = config_entry.options.get(
CONF_NO_FORCE_REFRESH_HOUR_START, DEFAULT_NO_FORCE_REFRESH_HOUR_START
)
self.no_force_refresh_hour_finish: int = config_entry.options.get(
CONF_NO_FORCE_REFRESH_HOUR_FINISH, DEFAULT_NO_FORCE_REFRESH_HOUR_FINISH
)
self.enable_geolocation_entity = config_entry.options.get(
CONF_ENABLE_GEOLOCATION_ENTITY, DEFAULT_ENABLE_GEOLOCATION_ENTITY
)
self.use_email_with_geocode_api = config_entry.options.get(
CONF_USE_EMAIL_WITH_GEOCODE_API, DEFAULT_USE_EMAIL_WITH_GEOCODE_API
)
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=timedelta(
seconds=min(self.scan_interval, self.force_refresh_interval)
),
)
async def _async_update_data(self):
"""Update data via library. Called by update_coordinator periodically.
Allow to update for the first time without further checking
Allow force update, if time diff between latest update and `now` is greater than force refresh delta
"""
await self.async_check_and_refresh_token()
current_hour = dt_util.now().hour
if (
(self.no_force_refresh_hour_start <= self.no_force_refresh_hour_finish)
and (
current_hour < self.no_force_refresh_hour_start
or current_hour >= self.no_force_refresh_hour_finish
)
) or (
(self.no_force_refresh_hour_start >= self.no_force_refresh_hour_finish)
and (
current_hour < self.no_force_refresh_hour_start
and current_hour >= self.no_force_refresh_hour_finish
)
):
try:
await self.hass.async_add_executor_job(
self.vehicle_manager.check_and_force_update_vehicles,
self.force_refresh_interval,
)
except Exception as err:
try:
await self.hass.async_add_executor_job(
self.vehicle_manager.update_all_vehicles_with_cached_state
)
_LOGGER.exception(
"Force update failed, falling back to cached: {err}"
)
except Exception as err_nested:
raise UpdateFailed(f"Error communicating with API: {err_nested}")
else:
await self.hass.async_add_executor_job(
self.vehicle_manager.update_all_vehicles_with_cached_state
)
return self.data
async def async_update_all(self) -> None:
"""Update vehicle data."""
await self.async_check_and_refresh_token()
await self.hass.async_add_executor_job(
self.vehicle_manager.update_all_vehicles_with_cached_state
)
await self.async_refresh()
async def async_force_update_all(self) -> None:
"""Force refresh vehicle data and update it."""
await self.async_check_and_refresh_token()
await self.hass.async_add_executor_job(
self.vehicle_manager.force_refresh_all_vehicles_states
)
await self.async_refresh()
async def async_check_and_refresh_token(self):
"""Refresh token if needed via library."""
await self.hass.async_add_executor_job(
self.vehicle_manager.check_and_refresh_token
)
async def async_lock_vehicle(self, vehicle_id: str):
await self.async_check_and_refresh_token()
await self.hass.async_add_executor_job(self.vehicle_manager.lock, vehicle_id)
await self.async_request_refresh()
async def async_unlock_vehicle(self, vehicle_id: str):
await self.async_check_and_refresh_token()
await self.hass.async_add_executor_job(self.vehicle_manager.unlock, vehicle_id)
await self.async_request_refresh()
async def async_open_charge_port(self, vehicle_id: str):
await self.async_check_and_refresh_token()
await self.hass.async_add_executor_job(
self.vehicle_manager.open_charge_port, vehicle_id
)
await self.async_request_refresh()
async def async_close_charge_port(self, vehicle_id: str):
await self.async_check_and_refresh_token()
await self.hass.async_add_executor_job(
self.vehicle_manager.close_charge_port, vehicle_id
)
await self.async_request_refresh()
async def async_start_climate(
self, vehicle_id: str, climate_options: ClimateRequestOptions
):
await self.async_check_and_refresh_token()
await self.hass.async_add_executor_job(
self.vehicle_manager.start_climate, vehicle_id, climate_options
)
await self.async_request_refresh()
async def async_stop_climate(self, vehicle_id: str):
await self.async_check_and_refresh_token()
await self.hass.async_add_executor_job(
self.vehicle_manager.stop_climate, vehicle_id
)
await self.async_request_refresh()
async def async_start_charge(self, vehicle_id: str):
await self.async_check_and_refresh_token()
await self.hass.async_add_executor_job(
self.vehicle_manager.stop_charge, vehicle_id
)
await self.async_request_refresh()
async def async_stop_charge(self, vehicle_id: str):
await self.async_check_and_refresh_token()
await self.hass.async_add_executor_job(
self.vehicle_manager.stop_charge, vehicle_id
)
await self.async_request_refresh()
async def set_charge_limits(self, vehicle_id: str, ac: int, dc: int):
await self.async_check_and_refresh_token()
await self.hass.async_add_executor_job(
self.vehicle_manager.set_charge_limits, vehicle_id, ac, dc
)
await self.async_request_refresh()

View File

@@ -0,0 +1,58 @@
"""Device Tracker for Hyundai / Kia Connect integration."""
from __future__ import annotations
import logging
from hyundai_kia_connect_api import Vehicle
from homeassistant.components.device_tracker import SOURCE_TYPE_GPS
from homeassistant.components.device_tracker.config_entry import TrackerEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .coordinator import HyundaiKiaConnectDataUpdateCoordinator
from .entity import HyundaiKiaConnectEntity
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
coordinator = hass.data[DOMAIN][config_entry.unique_id]
entities = []
for vehicle_id in coordinator.vehicle_manager.vehicles.keys():
vehicle: Vehicle = coordinator.vehicle_manager.vehicles[vehicle_id]
if vehicle.location is not None:
entities.append(HyundaiKiaConnectTracker(coordinator, vehicle))
async_add_entities(entities)
return True
class HyundaiKiaConnectTracker(TrackerEntity, HyundaiKiaConnectEntity):
def __init__(
self,
coordinator: HyundaiKiaConnectDataUpdateCoordinator,
vehicle: Vehicle,
):
HyundaiKiaConnectEntity.__init__(self, coordinator, vehicle)
self._attr_unique_id = f"{DOMAIN}_{vehicle.id}_location"
self._attr_name = f"{vehicle.name} Location"
self._attr_icon = "mdi:map-marker-outline"
@property
def latitude(self):
return self.vehicle.location_latitude
@property
def longitude(self):
return self.vehicle.location_longitude
@property
def source_type(self):
return SOURCE_TYPE_GPS

View File

@@ -0,0 +1,24 @@
"""Base Entity for Hyundai / Kia Connect integration."""
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.helpers.entity import DeviceInfo
from .const import BRANDS, DOMAIN, REGIONS
class HyundaiKiaConnectEntity(CoordinatorEntity):
"""Class for base entity for Hyundai / Kia Connect integration."""
def __init__(self, coordinator, vehicle):
"""Initialize the base entity."""
super().__init__(coordinator)
self.vehicle = vehicle
@property
def device_info(self):
"""Return device information to use for this entity."""
return DeviceInfo(
identifiers={(DOMAIN, self.vehicle.id)},
manufacturer=f"{BRANDS[self.coordinator.vehicle_manager.brand]} {REGIONS[self.coordinator.vehicle_manager.region]}",
model=self.vehicle.model,
name=self.vehicle.name,
)

View File

@@ -0,0 +1,54 @@
"""Lock for Hyundai / Kia Connect integration."""
from __future__ import annotations
import logging
from homeassistant.components.lock import LockEntity
from hyundai_kia_connect_api import Vehicle
from .const import DOMAIN
from .coordinator import HyundaiKiaConnectDataUpdateCoordinator
from .entity import HyundaiKiaConnectEntity
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
coordinator = hass.data[DOMAIN][config_entry.unique_id]
entities = []
for vehicle_id in coordinator.vehicle_manager.vehicles.keys():
vehicle: Vehicle = coordinator.vehicle_manager.vehicles[vehicle_id]
entities.append(HyundaiKiaConnectLock(coordinator, vehicle))
async_add_entities(entities)
return True
class HyundaiKiaConnectLock(LockEntity, HyundaiKiaConnectEntity):
def __init__(
self,
coordinator: HyundaiKiaConnectDataUpdateCoordinator,
vehicle: Vehicle,
):
HyundaiKiaConnectEntity.__init__(self, coordinator, vehicle)
self._attr_unique_id = f"{DOMAIN}_{vehicle.id}_door_lock"
self._attr_name = f"{vehicle.name} Door Lock"
@property
def icon(self):
return "mdi:lock" if self.is_locked else "mdi:lock-open-variant"
@property
def is_locked(self):
return getattr(self.vehicle, "is_locked")
async def async_lock(self):
await self.coordinator.async_lock_vehicle(self.vehicle.id)
async def async_unlock(self):
await self.coordinator.async_unlock_vehicle(self.vehicle.id)

View File

@@ -0,0 +1,12 @@
{
"domain": "kia_uvo",
"name": "Kia Uvo / Hyundai Bluelink",
"documentation": "https://github.com/fuatakgun/kia_uvo",
"issue_tracker": "https://github.com/fuatakgun/kia_uvo/issues",
"codeowners": ["@fuatakgun"],
"requirements": ["hyundai_kia_connect_api==1.45.6"],
"version": "2.0.50",
"config_flow": true,
"iot_class": "cloud_polling",
"integration_type": "hub"
}

View File

@@ -0,0 +1,134 @@
"""Number for Hyundai / Kia Connect integration."""
from __future__ import annotations
import logging
from typing import Final
from hyundai_kia_connect_api import Vehicle, VehicleManager
from homeassistant.components.number import NumberEntity, NumberEntityDescription
from homeassistant.const import PERCENTAGE
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN, DYNAMIC_UNIT
from .coordinator import HyundaiKiaConnectDataUpdateCoordinator
from .entity import HyundaiKiaConnectEntity
_LOGGER = logging.getLogger(__name__)
AC_CHARGING_LIMIT_KEY = "ev_charge_limits_ac"
DC_CHARGING_LIMIT_KEY = "ev_charge_limits_dc"
NUMBER_DESCRIPTIONS: Final[tuple[NumberEntityDescription, ...]] = (
NumberEntityDescription(
key=AC_CHARGING_LIMIT_KEY,
name="AC Charging Limit",
icon="mdi:ev-plug-type2",
native_min_value=50,
native_max_value=100,
native_step=10,
native_unit_of_measurement=PERCENTAGE,
),
NumberEntityDescription(
key=DC_CHARGING_LIMIT_KEY,
name="DC Charging Limit",
icon="mdi:ev-plug-ccs2",
native_min_value=50,
native_max_value=100,
native_step=10,
native_unit_of_measurement=PERCENTAGE,
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
coordinator = hass.data[DOMAIN][config_entry.unique_id]
entities = []
for vehicle_id in coordinator.vehicle_manager.vehicles.keys():
vehicle: Vehicle = coordinator.vehicle_manager.vehicles[vehicle_id]
for description in NUMBER_DESCRIPTIONS:
if getattr(vehicle, description.key, None) is not None:
entities.append(
HyundaiKiaConnectNumber(coordinator, description, vehicle)
)
async_add_entities(entities)
return True
class HyundaiKiaConnectNumber(NumberEntity, HyundaiKiaConnectEntity):
def __init__(
self,
coordinator: HyundaiKiaConnectDataUpdateCoordinator,
description: NumberEntityDescription,
vehicle: Vehicle,
) -> None:
super().__init__(coordinator, vehicle)
self._description = description
self._key = self._description.key
self._attr_unique_id = f"{DOMAIN}_{vehicle.id}_{self._key}"
self._attr_icon = self._description.icon
self._attr_name = f"{vehicle.name} {self._description.name}"
self._attr_device_class = self._description.device_class
@property
def native_value(self) -> float | None:
"""Return the entity value to represent the entity state."""
return getattr(self.vehicle, self._key)
async def async_set_native_value(self, value: float) -> None:
"""Set new charging limit."""
# force refresh of state so that we can get the value for the other charging limit
# since we have to set both limits as compound API call.
await self.coordinator.async_force_update_all()
if (
self._description.key == AC_CHARGING_LIMIT_KEY
and self.vehicle.ev_charge_limits_ac == int(value)
):
return
if (
self._description.key == DC_CHARGING_LIMIT_KEY
and self.vehicle.ev_charge_limits_dc == int(value)
):
return
# set new limits
if self._description.key == AC_CHARGING_LIMIT_KEY:
ac = value
dc = self.vehicle.ev_charge_limits_dc
else:
ac = self.vehicle.ev_charge_limits_ac
dc = value
await self.coordinator.set_charge_limits(self.vehicle.id, ac, dc)
self.async_write_ha_state()
@property
def native_min_value(self):
"""Return native_min_value as reported in by the sensor"""
return self._description.native_min_value
@property
def native_max_value(self):
"""Returnnative_max_value as reported in by the sensor"""
return self._description.native_max_value
@property
def native_step(self):
"""Return step value as reported in by the sensor"""
return self._description.native_step
@property
def native_unit_of_measurement(self):
"""Return the unit the value was reported in by the sensor"""
if self._description.native_unit_of_measurement == DYNAMIC_UNIT:
return getattr(self.vehicle, self._key + "_unit")
else:
return self._description.native_unit_of_measurement

View File

@@ -0,0 +1,274 @@
"""Sensor for Hyundai / Kia Connect integration."""
from __future__ import annotations
from collections.abc import Callable
import logging
from typing import Final
from hyundai_kia_connect_api import Vehicle
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import (
PERCENTAGE,
TIME_MINUTES,
ENERGY_WATT_HOUR,
ENERGY_KILO_WATT_HOUR,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN, DYNAMIC_UNIT
from .coordinator import HyundaiKiaConnectDataUpdateCoordinator
from .entity import HyundaiKiaConnectEntity
_LOGGER = logging.getLogger(__name__)
SENSOR_DESCRIPTIONS: Final[tuple[SensorEntityDescription, ...]] = (
SensorEntityDescription(
key="_total_driving_range",
name="Total Driving Range",
icon="mdi:road-variant",
device_class=SensorDeviceClass.DISTANCE,
native_unit_of_measurement=DYNAMIC_UNIT,
),
SensorEntityDescription(
key="_odometer",
name="Odometer",
icon="mdi:speedometer",
native_unit_of_measurement=DYNAMIC_UNIT,
device_class=SensorDeviceClass.DISTANCE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="_last_service_distance",
name="Last Service",
icon="mdi:car-wrench",
device_class=SensorDeviceClass.DISTANCE,
native_unit_of_measurement=DYNAMIC_UNIT,
),
SensorEntityDescription(
key="_next_service_distance",
name="Next Service",
icon="mdi:car-wrench",
device_class=SensorDeviceClass.DISTANCE,
native_unit_of_measurement=DYNAMIC_UNIT,
),
SensorEntityDescription(
key="car_battery_percentage",
name="Car Battery Level",
icon="mdi:car-battery",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="last_updated_at",
name="Last Updated At",
icon="mdi:update",
device_class=SensorDeviceClass.TIMESTAMP,
),
SensorEntityDescription(
key="ev_battery_percentage",
name="EV Battery Level",
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.BATTERY,
),
SensorEntityDescription(
key="_ev_driving_range",
name="EV Range",
icon="mdi:road-variant",
device_class=SensorDeviceClass.DISTANCE,
native_unit_of_measurement=DYNAMIC_UNIT,
),
SensorEntityDescription(
key="_fuel_driving_range",
name="Fuel Driving Range",
icon="mdi:road-variant",
device_class=SensorDeviceClass.DISTANCE,
native_unit_of_measurement=DYNAMIC_UNIT,
),
SensorEntityDescription(
key="fuel_level",
name="Fuel Level",
native_unit_of_measurement=PERCENTAGE,
icon="mdi:fuel",
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
key="_air_temperature",
name="Set Temperature",
native_unit_of_measurement=DYNAMIC_UNIT,
device_class=SensorDeviceClass.TEMPERATURE,
),
SensorEntityDescription(
key="ev_estimated_current_charge_duration",
name="Estimated Charge Duration",
icon="mdi:ev-station",
native_unit_of_measurement=TIME_MINUTES,
),
SensorEntityDescription(
key="ev_estimated_fast_charge_duration",
name="Estimated Fast Charge Duration",
icon="mdi:ev-station",
native_unit_of_measurement=TIME_MINUTES,
),
SensorEntityDescription(
key="ev_estimated_portable_charge_duration",
name="Estimated portable Charge Duration",
icon="mdi:ev-station",
native_unit_of_measurement=TIME_MINUTES,
),
SensorEntityDescription(
key="ev_estimated_station_charge_duration",
name="Estimated Station Charge Duration",
icon="mdi:ev-station",
native_unit_of_measurement=TIME_MINUTES,
),
SensorEntityDescription(
key="_ev_target_range_charge_AC",
name="Target Range of Charge AC",
icon="mdi:ev-station",
device_class=SensorDeviceClass.DISTANCE,
native_unit_of_measurement=DYNAMIC_UNIT,
),
SensorEntityDescription(
key="_ev_target_range_charge_DC",
name="Target Range of Charge DC",
icon="mdi:ev-station",
device_class=SensorDeviceClass.DISTANCE,
native_unit_of_measurement=DYNAMIC_UNIT,
),
SensorEntityDescription(
key="total_power_consumed",
name="Monthly Energy Consumption",
icon="mdi:car-electric",
native_unit_of_measurement=ENERGY_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
),
# Need to remove km hard coding. Underlying API needs this fixed first. EU always does KM.
SensorEntityDescription(
key="power_consumption_30d",
name="Average Energy Consumption",
icon="mdi:car-electric",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=f"{ENERGY_WATT_HOUR}/km",
),
SensorEntityDescription(
key="front_left_seat_status",
name="Front Left Seat",
icon="mdi:car-seat-heater",
),
SensorEntityDescription(
key="front_right_seat_status",
name="Front Right Seat",
icon="mdi:car-seat-heater",
),
SensorEntityDescription(
key="rear_left_seat_status",
name="Rear Left Seat",
icon="mdi:car-seat-heater",
),
SensorEntityDescription(
key="rear_right_seat_status",
name="Rear Right Seat",
icon="mdi:car-seat-heater",
),
SensorEntityDescription(
key="_geocode_name",
name="Geocoded Location",
icon="mdi:map",
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up sensor platform."""
coordinator = hass.data[DOMAIN][config_entry.unique_id]
entities = []
for vehicle_id in coordinator.vehicle_manager.vehicles.keys():
vehicle: Vehicle = coordinator.vehicle_manager.vehicles[vehicle_id]
for description in SENSOR_DESCRIPTIONS:
if getattr(vehicle, description.key, None) is not None:
entities.append(
HyundaiKiaConnectSensor(coordinator, description, vehicle)
)
async_add_entities(entities)
async_add_entities(
[VehicleEntity(coordinator, coordinator.vehicle_manager.vehicles[vehicle_id])],
True,
)
return True
class HyundaiKiaConnectSensor(SensorEntity, HyundaiKiaConnectEntity):
"""Hyundai / Kia Connect sensor class."""
def __init__(
self, coordinator, description: SensorEntityDescription, vehicle: Vehicle
):
"""Initialize the sensor."""
super().__init__(coordinator, vehicle)
self._description = description
self._key = self._description.key
self._attr_unique_id = f"{DOMAIN}_{vehicle.id}_{self._key}"
self._attr_icon = self._description.icon
self._attr_name = f"{vehicle.name} {self._description.name}"
self._attr_state_class = self._description.state_class
self._attr_device_class = self._description.device_class
@property
def native_value(self):
"""Return the value reported by the sensor."""
return getattr(self.vehicle, self._key)
@property
def native_unit_of_measurement(self):
"""Return the unit the value was reported in by the sensor"""
if self._description.native_unit_of_measurement == DYNAMIC_UNIT:
return getattr(self.vehicle, self._key + "_unit")
else:
return self._description.native_unit_of_measurement
@property
def state_attributes(self):
if self._description.key == "_geocode_name":
return {"address": getattr(self.vehicle, "_geocode_address")}
class VehicleEntity(SensorEntity, HyundaiKiaConnectEntity):
def __init__(self, coordinator, vehicle: Vehicle):
super().__init__(coordinator, vehicle)
@property
def state(self):
return "on"
@property
def is_on(self) -> bool:
return True
@property
def state_attributes(self):
return {
"vehicle_data": self.vehicle.data,
"vehicle_name": self.vehicle.name,
}
@property
def name(self):
return f"{self.vehicle.name} Data"
@property
def unique_id(self):
return f"{DOMAIN}-all-data-{self.vehicle.id}"

View File

@@ -0,0 +1,169 @@
import logging
from typing import Any, cast
from homeassistant.const import ATTR_DEVICE_ID
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import ServiceCall, callback, HomeAssistant
from .coordinator import HyundaiKiaConnectDataUpdateCoordinator
from homeassistant.helpers import device_registry
from hyundai_kia_connect_api import ClimateRequestOptions
from .const import DOMAIN
SERVICE_UPDATE = "update"
SERVICE_FORCE_UPDATE = "force_update"
SERVICE_LOCK = "lock"
SERVICE_UNLOCK = "unlock"
SERVICE_STOP_CLIMATE = "stop_climate"
SERVICE_START_CLIMATE = "start_climate"
SERVICE_START_CHARGE = "start_charge"
SERVICE_STOP_CHARGE = "stop_charge"
SERVICE_SET_CHARGE_LIMIT = "set_charge_limits"
SERVICE_OPEN_CHARGE_PORT = "open_charge_port"
SERVICE_CLOSE_CHARGE_PORT = "close_charge_port"
SUPPORTED_SERVICES = (
SERVICE_UPDATE,
SERVICE_FORCE_UPDATE,
SERVICE_LOCK,
SERVICE_UNLOCK,
SERVICE_STOP_CLIMATE,
SERVICE_START_CLIMATE,
SERVICE_START_CHARGE,
SERVICE_STOP_CHARGE,
SERVICE_SET_CHARGE_LIMIT,
SERVICE_OPEN_CHARGE_PORT,
SERVICE_CLOSE_CHARGE_PORT,
)
_LOGGER = logging.getLogger(__name__)
@callback
def async_setup_services(hass: HomeAssistant) -> bool:
"""Set up services for Hyundai Kia Connect"""
async def async_handle_force_update(call):
coordinator = _get_coordinator_from_device(hass, call)
await coordinator.async_force_update_all()
async def async_handle_update(call):
_LOGGER.debug(f"Call:{call.data}")
coordinator = _get_coordinator_from_device(hass, call)
await coordinator.async_update_all()
async def async_handle_start_climate(call):
coordinator = _get_coordinator_from_device(hass, call)
vehicle_id = _get_vehicle_id_from_device(hass, call)
climate_request_options = ClimateRequestOptions(
duration=call.data.get("duration"),
set_temp=call.data.get("temperature"),
climate=call.data.get("climate"),
heating=call.data.get("heating"),
defrost=call.data.get("defrost"),
front_left_seat=call.data.get("flseat"),
front_right_seat=call.data.get("frseat"),
rear_left_seat=call.data.get("rlseat"),
rear_right_seat=call.data.get("rrseat"),
)
await coordinator.async_start_climate(vehicle_id, climate_request_options)
async def async_handle_stop_climate(call):
coordinator = _get_coordinator_from_device(hass, call)
vehicle_id = _get_vehicle_id_from_device(hass, call)
await coordinator.async_stop_climate(vehicle_id)
async def async_handle_lock(call):
coordinator = _get_coordinator_from_device(hass, call)
vehicle_id = _get_vehicle_id_from_device(hass, call)
await coordinator.async_lock_vehicle(vehicle_id)
async def async_handle_unlock(call):
coordinator = _get_coordinator_from_device(hass, call)
vehicle_id = _get_vehicle_id_from_device(hass, call)
await coordinator.async_unlock_vehicle(vehicle_id)
async def async_handle_open_charge_port(call):
coordinator = _get_coordinator_from_device(hass, call)
vehicle_id = _get_vehicle_id_from_device(hass, call)
await coordinator.async_open_charge_port(vehicle_id)
async def async_handle_close_charge_port(call):
coordinator = _get_coordinator_from_device(hass, call)
vehicle_id = _get_vehicle_id_from_device(hass, call)
await coordinator.async_close_charge_port(vehicle_id)
async def async_handle_start_charge(call):
coordinator = _get_coordinator_from_device(hass, call)
vehicle_id = _get_vehicle_id_from_device(hass, call)
await coordinator.async_start_charge(vehicle_id)
async def async_handle_stop_charge(call):
coordinator = _get_coordinator_from_device(hass, call)
vehicle_id = _get_vehicle_id_from_device(hass, call)
await coordinator.async_stop_charge(vehicle_id)
async def async_handle_set_charge_limit(call):
coordinator = _get_coordinator_from_device(hass, call)
vehicle_id = _get_vehicle_id_from_device(hass, call)
ac = call.data.get("ac_limit")
dc = call.data.get("dc_limit")
if ac is not None or dc is not None:
await coordinator.set_charge_limits(vehicle_id, ac, dc)
services = {
SERVICE_FORCE_UPDATE: async_handle_force_update,
SERVICE_UPDATE: async_handle_update,
SERVICE_START_CLIMATE: async_handle_start_climate,
SERVICE_STOP_CLIMATE: async_handle_stop_climate,
SERVICE_LOCK: async_handle_lock,
SERVICE_UNLOCK: async_handle_unlock,
SERVICE_START_CHARGE: async_handle_start_charge,
SERVICE_STOP_CHARGE: async_handle_stop_charge,
SERVICE_SET_CHARGE_LIMIT: async_handle_set_charge_limit,
SERVICE_OPEN_CHARGE_PORT: async_handle_open_charge_port,
SERVICE_CLOSE_CHARGE_PORT: async_handle_close_charge_port,
}
for service in SUPPORTED_SERVICES:
hass.services.async_register(DOMAIN, service, services[service])
return True
@callback
def async_unload_services(hass) -> None:
for service in SUPPORTED_SERVICES:
hass.services.async_remove(DOMAIN, service)
def _get_vehicle_id_from_device(hass: HomeAssistant, call: ServiceCall) -> str:
device_entry = device_registry.async_get(hass).async_get(call.data[ATTR_DEVICE_ID])
for entry in device_entry.identifiers:
if entry[0] == DOMAIN:
vehicle_id = entry[1]
return vehicle_id
def _get_coordinator_from_device(
hass: HomeAssistant, call: ServiceCall
) -> HyundaiKiaConnectDataUpdateCoordinator:
device_entry = device_registry.async_get(hass).async_get(call.data[ATTR_DEVICE_ID])
config_entry_ids = device_entry.config_entries
config_entry_id = next(
(
config_entry_id
for config_entry_id in config_entry_ids
if cast(
ConfigEntry,
hass.config_entries.async_get_entry(config_entry_id),
).domain
== DOMAIN
),
None,
)
config_entry_unique_id = hass.config_entries.async_get_entry(
config_entry_id
).unique_id
return hass.data[DOMAIN][config_entry_unique_id]

View File

@@ -0,0 +1,282 @@
force_update:
description: Force your vehicle to update its data. All vehicles on the same account as the vehicle selected will be updated.
fields:
device_id:
name: Vehicle
description: Target vehicle
required: true
selector:
device:
integration: kia_uvo
update:
description: Update vehicle data from service cache
fields:
device_id:
name: Vehicle
description: Target vehicle
required: true
selector:
device:
integration: kia_uvo
start_climate:
description: Please use cautiously - Starts climate and engine. Not all options are available on all cars or regions. Use your cars mobile app as a guide and match the options available in your car.
fields:
device_id:
name: Vehicle
description: Target vehicle
required: true
selector:
device:
integration: kia_uvo
duration:
name: Duration
description: On Duration
required: false
example: 5
default: 5
selector:
number:
min: 1
max: 10
step: 1
unit_of_measurement: minutes
climate:
name: Climate
description: Enable the HVAC System
required: true
default: true
selector:
boolean:
temperature:
name: Temperature
description: Set temperature of climate control. Unit is specific to region.
required: true
example: 21.5
default: 21
selector:
number:
min: 16
max: 85
step: 0.5
mode: box
unit_of_measurement: Degrees
defrost:
name: Defrost
description: Front Windshield Defrost
required: false
default: false
selector:
boolean:
heating:
name: Heating
description: Heated features like the steering wheel and rear window
required: true
example: false
default: false
selector:
select:
options:
- label: "Off"
value: "0"
- label: "Steering Wheel, Side and Back Defroster"
value: "1"
- label: "Rear Window Only"
value: "2"
- label: "Steering Wheel Only"
value: "3"
flseat:
name: Front Left Seat
description: Front Left Seat Heat Cool Setting
required: false
selector:
select:
options:
- label: "Off"
value: "0"
- label: "On"
value: "1"
- label: "Low Cool"
value: "3"
- label: "Medium Cool"
value: "4"
- label: "High Cool"
value: "5"
- label: "Low Heat"
value: "6"
- label: "Medium Heat"
value: "7"
- label: "High Heat"
value: "8"
frseat:
name: Front Right Seat
description: Front Right Seat Heat Cool Setting
required: false
selector:
select:
options:
- label: "Off"
value: "0"
- label: "On"
value: "1"
- label: "Low Cool"
value: "3"
- label: "Medium Cool"
value: "4"
- label: "High Cool"
value: "5"
- label: "Low Heat"
value: "6"
- label: "Medium Heat"
value: "7"
- label: "High Heat"
value: "8"
rlseat:
name: Rear Left Seat
description: Rear Left Seat Heat Cool Setting
required: false
selector:
select:
options:
- label: "Off"
value: "0"
- label: "On"
value: "1"
- label: "Low Cool"
value: "3"
- label: "Medium Cool"
value: "4"
- label: "High Cool"
value: "5"
- label: "Low Heat"
value: "6"
- label: "Medium Heat"
value: "7"
- label: "High Heat"
value: "8"
rrseat:
name: Rear Right Seat
description: Rear Rear Seat Heat Cool Setting
required: false
selector:
select:
options:
- label: "Off"
value: "0"
- label: "On"
value: "1"
- label: "Low Cool"
value: "3"
- label: "Medium Cool"
value: "4"
- label: "High Cool"
value: "5"
- label: "Low Heat"
value: "6"
- label: "Medium Heat"
value: "7"
- label: "High Heat"
value: "8"
stop_climate:
description: Please use cautiously - stop car and climate
fields:
device_id:
name: Vehicle
description: Target vehicle
required: true
selector:
device:
integration: kia_uvo
start_charge:
description: Start charging
fields:
device_id:
name: Vehicle
description: Target vehicle
required: true
selector:
device:
integration: kia_uvo
stop_charge:
description: Stop charging
fields:
device_id:
name: Vehicle
description: Target vehicle
required: true
selector:
device:
integration: kia_uvo
lock:
description: Lock the vehicle
fields:
device_id:
name: Vehicle
description: Target vehicle
required: true
selector:
device:
integration: kia_uvo
unlock:
description: Unlock the vehicle
fields:
device_id:
name: Vehicle
description: Target vehicle
required: true
selector:
device:
integration: kia_uvo
close_charge_port:
description: Close Charge Port
fields:
device_id:
name: Vehicle
description: Target vehicle
required: true
selector:
device:
integration: kia_uvo
open_charge_port:
description: Open Charge Port
fields:
device_id:
name: Vehicle
description: Target vehicle
required: true
selector:
device:
integration: kia_uvo
set_charge_limits:
description: sets ac and dc charge capacity limits
fields:
device_id:
name: Vehicle
description: Target vehicle
required: true
selector:
device:
integration: kia_uvo
dc_limit:
name: DC Charge limit
description: max charge capacity using DC charger
required: false
example: 50
default: 90
selector:
number:
min: 50
max: 100
step: 10
unit_of_measurement: '%'
ac_limit:
name: AC Charge limit
description: max charge capacity using AC charger
required: false
example: 50
default: 90
selector:
number:
min: 50
max: 100
step: 10
unit_of_measurement: '%'

View File

@@ -0,0 +1,34 @@
{
"config": {
"step": {
"user": {
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
"region": "[%key:component::hyundai_kia_connect::config::step::user::data::region%]",
"brand": "[%key:component::hyundai_kia_connect::config::step::user::data::brand%]",
"pin": "[%key:common::config_flow::data::pin%]"
}
}
},
"error": {
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"options": {
"step": {
"init": {
"data": {
"scan_interval": "[%key:component::hyundai_kia_connect::options::step::init::data::scan_interval%]",
"force_refresh": "[%key:component::hyundai_kia_connect::options::step::init::data::force_refresh%]",
"no_force_refresh_hour_start": "[%key:component::hyundai_kia_connect::options::step::init::data::no_force_refresh_hour_start%]",
"no_force_refresh_hour_finish": "[%key:component::hyundai_kia_connect::options::step::init::data::no_force_refresh_hour_finish%]"
}
}
}
}
}

View File

@@ -0,0 +1,40 @@
{
"title": "Hyundai / Kia Connect",
"config": {
"step": {
"user": {
"title": "Hyundai / Kia Connect - Authentication",
"description": "Set up your Hyundai (Bluelink) / Kia (Uvo) Connect to integrate with Home Assistant.",
"data": {
"username": "Username",
"password": "Password",
"region": "Region",
"brand": "Brand",
"pin": "Pin (Required for CA)"
}
}
},
"abort": {
"already_configured": "Device is already configured"
},
"error": {
"invalid_auth": "Login failed into Hyundai (Bluelink) / Kia (Uvo) Connect Servers. Please use official app to logout and log back in and try again!",
"unknown": "Unexpected error"
}
},
"options": {
"step": {
"init": {
"title": "Hyundai / Kia Connect - Configuration",
"data": {
"scan_interval": "Scan Interval (min)",
"force_refresh": "Force Refresh Interval (min)",
"no_force_refresh_hour_start": "No Force Refresh Start Hour",
"no_force_refresh_hour_finish": "No Force Refresh Finish Hour",
"enable_geolocation_entity": "Enable Geolocation Entity using OpenStreetMap",
"use_email_with_geocode_api": "Use your Kia email address for Geocode API - More Information: https://nominatim.org/release-docs/develop/api/Reverse/#other"
}
}
}
}
}