initial commit
This commit is contained in:
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
)
|
||||
@@ -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()
|
||||
@@ -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."""
|
||||
@@ -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"
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
@@ -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}"
|
||||
@@ -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]
|
||||
@@ -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: '%'
|
||||
@@ -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%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user