Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added support for multiple accounts #206

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
37 changes: 30 additions & 7 deletions custom_components/kia_uvo/HyundaiBlueLinkAPIUSA.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import uuid
import time
import curlify
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.ssl_ import create_urllib3_context

from .const import (
DOMAIN,
Expand All @@ -22,9 +24,28 @@
from .KiaUvoApiImpl import KiaUvoApiImpl
from .Token import Token


CIPHERS = "DEFAULT@SECLEVEL=1"

_LOGGER = logging.getLogger(__name__)


class cipherAdapter(HTTPAdapter):
"""
A HTTPAdapter that re-enables poor ciphers required by Hyundai.
"""

def init_poolmanager(self, *args, **kwargs):
context = create_urllib3_context(ciphers=CIPHERS)
kwargs["ssl_context"] = context
return super().init_poolmanager(*args, **kwargs)

def proxy_manager_for(self, *args, **kwargs):
context = create_urllib3_context(ciphers=CIPHERS)
kwargs["ssl_context"] = context
return super().proxy_manager_for(*args, **kwargs)


class HyundaiBlueLinkAPIUSA(KiaUvoApiImpl):

old_vehicle_status = None
Expand Down Expand Up @@ -79,6 +100,8 @@ def __init__(
"client_id": "m66129Bb-em93-SPAHYN-bZ91-am4540zp19920",
"clientSecret": "v558o935-6nne-423i-baa8",
}
self.sessions = requests.Session()
self.sessions.mount("https://" + self.BASE_URL, cipherAdapter())

_LOGGER.debug(f"{DOMAIN} - initial API headers: {self.API_HEADERS}")

Expand All @@ -92,7 +115,7 @@ def login(self) -> Token:

data = {"username": username, "password": password}
headers = self.API_HEADERS
response = requests.post(url, json=data, headers=headers)
response = self.sessions.post(url, json=data, headers=headers)
_LOGGER.debug(f"{DOMAIN} - Sign In Response {response.text}")
response = response.json()
access_token = response["access_token"]
Expand Down Expand Up @@ -142,7 +165,7 @@ def get_cached_vehicle_status(self, token: Token):

_LOGGER.debug(f"{DOMAIN} - using API headers: {self.API_HEADERS}")

response = requests.get(url, headers=headers)
response = self.sessions.get(url, headers=headers)
response = response.json()
_LOGGER.debug(f"{DOMAIN} - get_cached_vehicle_status response {response}")

Expand Down Expand Up @@ -235,7 +258,7 @@ def get_location(self, token: Token, current_odometer):
if check_server:
try:
HyundaiBlueLinkAPIUSA.last_loc_timestamp = datetime.now()
response = requests.get(url, headers=headers)
response = self.sessions.get(url, headers=headers)
response_json = response.json()
_LOGGER.debug(f"{DOMAIN} - Get Vehicle Location {response_json}")
if response_json.get("coord") is not None:
Expand Down Expand Up @@ -287,7 +310,7 @@ def get_vehicle(self, access_token):
url = self.API_URL + "enrollment/details/" + username
headers = self.API_HEADERS
headers["accessToken"] = access_token
response = requests.get(url, headers=headers)
response = self.sessions.get(url, headers=headers)
_LOGGER.debug(f"{DOMAIN} - Get Vehicles Response {response.text}")
response = response.json()

Expand Down Expand Up @@ -316,7 +339,7 @@ def lock_action(self, token: Token, action):
headers["APPCLOUD-VIN"] = token.vehicle_id

data = {"userName": self.username, "vin": token.vehicle_id}
response = requests.post(url, headers=headers, json=data)
response = self.sessions.post(url, headers=headers, json=data)
# response_headers = response.headers
# response = response.json()
# action_status = self.check_action_status(token, headers["pAuth"], response_headers["transactionId"])
Expand Down Expand Up @@ -353,7 +376,7 @@ def start_climate(
}
_LOGGER.debug(f"{DOMAIN} - Start engine data: {data}")

response = requests.post(url, json=data, headers=headers)
response = self.sessions.post(url, json=data, headers=headers)

# _LOGGER.debug(f"{DOMAIN} - Start engine curl: {curlify.to_curl(response.request)}")
_LOGGER.debug(
Expand All @@ -373,7 +396,7 @@ def stop_climate(self, token: Token):

_LOGGER.debug(f"{DOMAIN} - Stop engine headers: {headers}")

response = requests.post(url, headers=headers)
response = self.sessions.post(url, headers=headers)
_LOGGER.debug(
f"{DOMAIN} - Stop engine response status code: {response.status_code}"
)
Expand Down
14 changes: 8 additions & 6 deletions custom_components/kia_uvo/KiaUvoAPIUSA.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,9 +260,10 @@ def get_cached_vehicle_status(self, token: Token):

vehicle_status["time"] = vehicle_status["syncDate"]["utc"]

vehicle_status["battery"] = {
"batSoc": vehicle_status["batteryStatus"]["stateOfCharge"],
}
if vehicle_status["batteryStatus"].get("stateOfCharge"):
vehicle_status["battery"] = {
"batSoc": vehicle_status["batteryStatus"]["stateOfCharge"],
}

if vehicle_status.get("evStatus"):
vehicle_status["evStatus"]["remainTime2"] = {
Expand All @@ -273,9 +274,10 @@ def get_cached_vehicle_status(self, token: Token):
vehicle_status["trunkOpen"] = vehicle_status["doorStatus"]["trunk"]
vehicle_status["hoodOpen"] = vehicle_status["doorStatus"]["hood"]

vehicle_status["tirePressureLamp"] = {
"tirePressureLampAll": vehicle_status["tirePressure"]["all"]
}
if vehicle_status.get("tirePressure"):
vehicle_status["tirePressureLamp"] = {
"tirePressureLampAll": vehicle_status["tirePressure"]["all"]
}

climate_data = vehicle_status["climate"]
vehicle_status["airCtrlOn"] = climate_data["airCtrl"]
Expand Down
27 changes: 22 additions & 5 deletions custom_components/kia_uvo/KiaUvoApiCA.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,16 @@ def login(self) -> Token:

return token

def get_vehicles(self, token: Token):
url = self.API_URL + "vhcllst"
headers = self.API_HEADERS
headers["accessToken"] = token.access_token
response = requests.post(url, headers=headers)
_LOGGER.debug(f"{DOMAIN} - Get Vehicles Response {response.text}")
response = response.json()
response = response["result"]
return response

def get_cached_vehicle_status(self, token: Token):
# Vehicle Status Call
url = self.API_URL + "lstvhclsts"
Expand All @@ -127,11 +137,7 @@ def get_cached_vehicle_status(self, token: Token):
vehicle_status["vehicleStatus"]["time"] = response["lastStatusDate"]

# Service Status Call
url = self.API_URL + "nxtsvc"
response = requests.post(url, headers=headers)
response = response.json()
_LOGGER.debug(f"{DOMAIN} - Get Service status data {response}")
response = response["result"]["maintenanceInfo"]
response = self.get_next_service(token)

vehicle_status["odometer"] = {}
vehicle_status["odometer"]["unit"] = response["currentOdometerUnit"]
Expand Down Expand Up @@ -163,6 +169,17 @@ def get_cached_vehicle_status(self, token: Token):
self.old_vehicle_status = vehicle_status
return vehicle_status

def get_next_service(self, token: Token):
headers = self.API_HEADERS
headers["accessToken"] = token.access_token
headers["vehicleId"] = token.vehicle_id
url = self.API_URL + "nxtsvc"
response = requests.post(url, headers=headers)
response = response.json()
_LOGGER.debug(f"{DOMAIN} - Get Service status data {response}")
response = response["result"]["maintenanceInfo"]
return response

def get_location(self, token: Token):
url = self.API_URL + "fndmcr"
headers = self.API_HEADERS
Expand Down
3 changes: 3 additions & 0 deletions custom_components/kia_uvo/KiaUvoApiImpl.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ def __init__(
def login(self) -> Token:
pass

def get_vehicles(self, token: Token):
pass

def get_cached_vehicle_status(self, token: Token):
pass

Expand Down
10 changes: 8 additions & 2 deletions custom_components/kia_uvo/KiaUvoEntity.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
from homeassistant.helpers.entity import Entity

from .Vehicle import Vehicle
from .const import DOMAIN, DATA_VEHICLE_INSTANCE, TOPIC_UPDATE, BRANDS
from .const import (
DOMAIN,
DATA_VEHICLE_INSTANCE,
TOPIC_UPDATE,
BRANDS,
CONF_VEHICLE_IDENTIFIER,
)


class KiaUvoEntity(Entity):
Expand Down Expand Up @@ -49,4 +55,4 @@ def device_info(self):

@callback
def update_from_latest_data(self):
self.vehicle = self.hass.data[DOMAIN][DATA_VEHICLE_INSTANCE]
self.vehicle = self.hass.data[DOMAIN][self.vehicle.id][DATA_VEHICLE_INSTANCE]
10 changes: 8 additions & 2 deletions custom_components/kia_uvo/Vehicle.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,10 @@ async def start_climate(self, set_temp, duration, defrost, climate, heating):
climate = True
if heating is None:
heating = False
if self.engine_type == VEHICLE_ENGINE_TYPE.EV and self.region == REGION_CANADA:
if (
self.engine_type == VEHICLE_ENGINE_TYPE.EV
and REGIONS[self.region] == REGION_CANADA
):
await self.hass.async_add_executor_job(
self.kia_uvo_api.start_climate_ev,
self.token,
Expand All @@ -213,7 +216,10 @@ async def start_climate(self, set_temp, duration, defrost, climate, heating):
await self.force_update_loop_start()

async def stop_climate(self):
if self.engine_type == VEHICLE_ENGINE_TYPE.EV and self.region == REGION_CANADA:
if (
self.engine_type == VEHICLE_ENGINE_TYPE.EV
and REGIONS[self.region] == REGION_CANADA
):
await self.hass.async_add_executor_job(
self.kia_uvo_api.stop_climate_ev, self.token
)
Expand Down
66 changes: 52 additions & 14 deletions custom_components/kia_uvo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
CONF_USERNAME,
CONF_UNIT_OF_MEASUREMENT,
CONF_REGION,
ATTR_DEVICE_ID,
)
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
Expand Down Expand Up @@ -41,6 +42,7 @@
CONF_NO_FORCE_SCAN_HOUR_START,
CONF_SCAN_INTERVAL,
CONF_STORED_CREDENTIALS,
CONF_VEHICLE_IDENTIFIER,
DISTANCE_UNITS,
DEFAULT_NO_FORCE_SCAN_HOUR_FINISH,
DEFAULT_NO_FORCE_SCAN_HOUR_START,
Expand Down Expand Up @@ -71,15 +73,30 @@


async def async_setup(hass: HomeAssistant, config_entry: ConfigEntry):
if DOMAIN not in hass.data:
hass.data[DOMAIN] = {}
hass.data.setdefault(DOMAIN, {})

def get_vehicle_identifier_from_vehicle_id(call) -> Vehicle:
vehicle_identifiers = list(hass.data[DOMAIN].keys())
if len(vehicle_identifiers) == 1:
Copy link
Member

Choose a reason for hiding this comment

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

i would get rid of if condition here, else should work for all conditions.

vehicle_identifier = vehicle_identifiers[0]
else:
vehicle_identifier = get_vehicle_identifier_from_device_id(
call.data[ATTR_DEVICE_ID]
)

return hass.data[DOMAIN][vehicle_identifier][DATA_VEHICLE_INSTANCE]

def get_vehicle_identifier_from_device_id(device_id: str) -> str:
device_registry = dr.async_get(hass)
device_entry: dr.DeviceEntry = device_registry.async_get(device_id)
return list(device_entry.identifiers.copy().pop())[1]

async def async_handle_force_update(call):
vehicle: Vehicle = hass.data[DOMAIN][DATA_VEHICLE_INSTANCE]
vehicle: Vehicle = get_vehicle_identifier_from_vehicle_id(call)
await vehicle.force_update()

async def async_handle_update(call):
vehicle: Vehicle = hass.data[DOMAIN][DATA_VEHICLE_INSTANCE]
vehicle: Vehicle = get_vehicle_identifier_from_vehicle_id(call)
await vehicle.update()

async def async_handle_start_climate(call):
Expand All @@ -88,25 +105,25 @@ async def async_handle_start_climate(call):
defrost = call.data.get("Defrost")
climate = call.data.get("Climate")
heating = call.data.get("Heating")
vehicle: Vehicle = hass.data[DOMAIN][DATA_VEHICLE_INSTANCE]
vehicle: Vehicle = get_vehicle_identifier_from_vehicle_id(call)
await vehicle.start_climate(set_temp, duration, defrost, climate, heating)

async def async_handle_stop_climate(call):
vehicle: Vehicle = hass.data[DOMAIN][DATA_VEHICLE_INSTANCE]
vehicle: Vehicle = get_vehicle_identifier_from_vehicle_id(call)
await vehicle.stop_climate()

async def async_handle_start_charge(call):
vehicle: Vehicle = hass.data[DOMAIN][DATA_VEHICLE_INSTANCE]
vehicle: Vehicle = get_vehicle_identifier_from_vehicle_id(call)
await vehicle.start_charge()

async def async_handle_stop_charge(call):
vehicle: Vehicle = hass.data[DOMAIN][DATA_VEHICLE_INSTANCE]
vehicle: Vehicle = get_vehicle_identifier_from_vehicle_id(call)
await vehicle.stop_charge()

async def async_handle_set_charge_limits(call):
ac_limit = call.data.get("ac_limit")
dc_limit = call.data.get("dc_limit")
vehicle: Vehicle = hass.data[DOMAIN][DATA_VEHICLE_INSTANCE]
vehicle: Vehicle = get_vehicle_identifier_from_vehicle_id(call)
await vehicle.set_charge_limits(ac_limit, dc_limit)

hass.services.async_register(DOMAIN, "force_update", async_handle_force_update)
Expand All @@ -124,6 +141,7 @@ async def async_handle_set_charge_limits(call):

async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry):
_LOGGER.debug(f"{DOMAIN} - async_setup_entry started - {config_entry}")
vehicle_identifier = config_entry.data[CONF_VEHICLE_IDENTIFIER]
username = config_entry.data.get(CONF_USERNAME)
password = config_entry.data.get(CONF_PASSWORD)
pin = config_entry.data.get(CONF_PIN, DEFAULT_PIN)
Expand Down Expand Up @@ -193,8 +211,10 @@ async def refresh_config_entry():

async def update(event_time_utc: datetime):
await refresh_config_entry()
# await vehicle.refresh_token()
local_timezone = vehicle.kia_uvo_api.get_timezone_by_region()
event_time_local = dt_util.as_local(event_time_utc)

await vehicle.update()
call_force_update = False

Expand Down Expand Up @@ -225,7 +245,7 @@ async def update(event_time_utc: datetime):
data[DATA_CONFIG_UPDATE_LISTENER] = config_entry.add_update_listener(
async_update_options
)
hass.data[DOMAIN] = data
hass.data[DOMAIN][vehicle_identifier] = data

return True

Expand All @@ -244,12 +264,30 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry):
)
)
if unload_ok:
vehicle_topic_listener = hass.data[DOMAIN][DATA_VEHICLE_LISTENER]
vehicle_topic_listener()
vehicle_identifier = config_entry.data[CONF_VEHICLE_IDENTIFIER]
vehicle_listener = hass.data[DOMAIN][vehicle_identifier][DATA_VEHICLE_LISTENER]
vehicle_listener()

config_update_listener = hass.data[DOMAIN][DATA_CONFIG_UPDATE_LISTENER]
config_update_listener = hass.data[DOMAIN][vehicle_identifier][
DATA_CONFIG_UPDATE_LISTENER
]
config_update_listener()

hass.data[DOMAIN] = None
hass.data[DOMAIN][vehicle_identifier] = None

return unload_ok


async def async_migrate_entry(hass, config_entry: ConfigEntry):
Copy link
Member

Choose a reason for hiding this comment

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

very nice approach, thanks for saving existing users


if config_entry.version == 1:

vehicle_id = config_entry.data["stored_credentials"]["vehicle_id"]
new_data = config_entry.data.copy()
new_data[CONF_VEHICLE_IDENTIFIER] = vehicle_id
Copy link
Member

Choose a reason for hiding this comment

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

looking here, vehicle_id should be equal to device_id. So, are these equal?
device_registry.async_get(device_id) to vehicle_id. I personally do not know.


hass.config_entries.async_update_entry(config_entry, data=new_data)

config_entry.version = 2
_LOGGER.info("Migration to version %s successful", config_entry.version)
return True