initial commit
This commit is contained in:
121
custom_components/kia_uvo/__init__.py
Normal file
121
custom_components/kia_uvo/__init__.py
Normal 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
|
||||
BIN
custom_components/kia_uvo/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
custom_components/kia_uvo/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
custom_components/kia_uvo/__pycache__/const.cpython-310.pyc
Normal file
BIN
custom_components/kia_uvo/__pycache__/const.cpython-310.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
custom_components/kia_uvo/__pycache__/entity.cpython-310.pyc
Normal file
BIN
custom_components/kia_uvo/__pycache__/entity.cpython-310.pyc
Normal file
Binary file not shown.
BIN
custom_components/kia_uvo/__pycache__/lock.cpython-310.pyc
Normal file
BIN
custom_components/kia_uvo/__pycache__/lock.cpython-310.pyc
Normal file
Binary file not shown.
BIN
custom_components/kia_uvo/__pycache__/number.cpython-310.pyc
Normal file
BIN
custom_components/kia_uvo/__pycache__/number.cpython-310.pyc
Normal file
Binary file not shown.
BIN
custom_components/kia_uvo/__pycache__/sensor.cpython-310.pyc
Normal file
BIN
custom_components/kia_uvo/__pycache__/sensor.cpython-310.pyc
Normal file
Binary file not shown.
BIN
custom_components/kia_uvo/__pycache__/services.cpython-310.pyc
Normal file
BIN
custom_components/kia_uvo/__pycache__/services.cpython-310.pyc
Normal file
Binary file not shown.
265
custom_components/kia_uvo/binary_sensor.py
Normal file
265
custom_components/kia_uvo/binary_sensor.py
Normal 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
|
||||
)
|
||||
242
custom_components/kia_uvo/climate.py
Normal file
242
custom_components/kia_uvo/climate.py
Normal 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()
|
||||
177
custom_components/kia_uvo/config_flow.py
Normal file
177
custom_components/kia_uvo/config_flow.py
Normal 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."""
|
||||
28
custom_components/kia_uvo/const.py
Normal file
28
custom_components/kia_uvo/const.py
Normal 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"
|
||||
221
custom_components/kia_uvo/coordinator.py
Normal file
221
custom_components/kia_uvo/coordinator.py
Normal 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()
|
||||
58
custom_components/kia_uvo/device_tracker.py
Normal file
58
custom_components/kia_uvo/device_tracker.py
Normal 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
|
||||
24
custom_components/kia_uvo/entity.py
Normal file
24
custom_components/kia_uvo/entity.py
Normal 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,
|
||||
)
|
||||
54
custom_components/kia_uvo/lock.py
Normal file
54
custom_components/kia_uvo/lock.py
Normal 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)
|
||||
12
custom_components/kia_uvo/manifest.json
Normal file
12
custom_components/kia_uvo/manifest.json
Normal 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"
|
||||
}
|
||||
134
custom_components/kia_uvo/number.py
Normal file
134
custom_components/kia_uvo/number.py
Normal 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
|
||||
274
custom_components/kia_uvo/sensor.py
Normal file
274
custom_components/kia_uvo/sensor.py
Normal 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}"
|
||||
169
custom_components/kia_uvo/services.py
Normal file
169
custom_components/kia_uvo/services.py
Normal 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]
|
||||
282
custom_components/kia_uvo/services.yaml
Normal file
282
custom_components/kia_uvo/services.yaml
Normal 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: '%'
|
||||
34
custom_components/kia_uvo/strings.json
Normal file
34
custom_components/kia_uvo/strings.json
Normal 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%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
40
custom_components/kia_uvo/translations/en.json
Normal file
40
custom_components/kia_uvo/translations/en.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user