mirror of
https://github.com/home-assistant/core.git
synced 2025-04-19 14:57:52 +00:00
Add renault integration (#39605)
This commit is contained in:
parent
7bd46b7705
commit
8d84edd3b7
@ -81,6 +81,7 @@ homeassistant.components.recorder.purge
|
||||
homeassistant.components.recorder.repack
|
||||
homeassistant.components.recorder.statistics
|
||||
homeassistant.components.remote.*
|
||||
homeassistant.components.renault.*
|
||||
homeassistant.components.rituals_perfume_genie.*
|
||||
homeassistant.components.scene.*
|
||||
homeassistant.components.select.*
|
||||
|
@ -411,6 +411,7 @@ homeassistant/components/rainmachine/* @bachya
|
||||
homeassistant/components/random/* @fabaff
|
||||
homeassistant/components/recollect_waste/* @bachya
|
||||
homeassistant/components/rejseplanen/* @DarkFox
|
||||
homeassistant/components/renault/* @epenet
|
||||
homeassistant/components/repetier/* @MTrab
|
||||
homeassistant/components/rflink/* @javicalle
|
||||
homeassistant/components/rfxtrx/* @danielhiversen @elupus @RobBie1221
|
||||
|
45
homeassistant/components/renault/__init__.py
Normal file
45
homeassistant/components/renault/__init__.py
Normal file
@ -0,0 +1,45 @@
|
||||
"""Support for Renault devices."""
|
||||
import aiohttp
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import CONF_LOCALE, DOMAIN, PLATFORMS
|
||||
from .renault_hub import RenaultHub
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Load a config entry."""
|
||||
renault_hub = RenaultHub(hass, config_entry.data[CONF_LOCALE])
|
||||
try:
|
||||
login_success = await renault_hub.attempt_login(
|
||||
config_entry.data[CONF_USERNAME], config_entry.data[CONF_PASSWORD]
|
||||
)
|
||||
except aiohttp.ClientConnectionError as exc:
|
||||
raise ConfigEntryNotReady() from exc
|
||||
|
||||
if not login_success:
|
||||
return False
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
await renault_hub.async_initialise(config_entry)
|
||||
|
||||
hass.data[DOMAIN][config_entry.unique_id] = renault_hub
|
||||
|
||||
hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(
|
||||
config_entry, PLATFORMS
|
||||
)
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(config_entry.unique_id)
|
||||
|
||||
return unload_ok
|
92
homeassistant/components/renault/config_flow.py
Normal file
92
homeassistant/components/renault/config_flow.py
Normal file
@ -0,0 +1,92 @@
|
||||
"""Config flow to configure Renault component."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from renault_api.const import AVAILABLE_LOCALES
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
|
||||
from .const import CONF_KAMEREON_ACCOUNT_ID, CONF_LOCALE, DOMAIN
|
||||
from .renault_hub import RenaultHub
|
||||
|
||||
|
||||
class RenaultFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a Renault config flow."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the Renault config flow."""
|
||||
self.renault_config: dict[str, Any] = {}
|
||||
self.renault_hub: RenaultHub | None = None
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle a Renault config flow start.
|
||||
|
||||
Ask the user for API keys.
|
||||
"""
|
||||
if user_input:
|
||||
locale = user_input[CONF_LOCALE]
|
||||
self.renault_config.update(user_input)
|
||||
self.renault_config.update(AVAILABLE_LOCALES[locale])
|
||||
self.renault_hub = RenaultHub(self.hass, locale)
|
||||
if not await self.renault_hub.attempt_login(
|
||||
user_input[CONF_USERNAME], user_input[CONF_PASSWORD]
|
||||
):
|
||||
return self._show_user_form({"base": "invalid_credentials"})
|
||||
return await self.async_step_kamereon()
|
||||
return self._show_user_form()
|
||||
|
||||
def _show_user_form(self, errors: dict[str, Any] | None = None) -> FlowResult:
|
||||
"""Show the API keys form."""
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_LOCALE): vol.In(AVAILABLE_LOCALES.keys()),
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
),
|
||||
errors=errors or {},
|
||||
)
|
||||
|
||||
async def async_step_kamereon(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Select Kamereon account."""
|
||||
if user_input:
|
||||
await self.async_set_unique_id(user_input[CONF_KAMEREON_ACCOUNT_ID])
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
self.renault_config.update(user_input)
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_KAMEREON_ACCOUNT_ID], data=self.renault_config
|
||||
)
|
||||
|
||||
assert self.renault_hub
|
||||
accounts = await self.renault_hub.get_account_ids()
|
||||
if len(accounts) == 0:
|
||||
return self.async_abort(reason="kamereon_no_account")
|
||||
if len(accounts) == 1:
|
||||
await self.async_set_unique_id(accounts[0])
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
self.renault_config[CONF_KAMEREON_ACCOUNT_ID] = accounts[0]
|
||||
return self.async_create_entry(
|
||||
title=self.renault_config[CONF_KAMEREON_ACCOUNT_ID],
|
||||
data=self.renault_config,
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="kamereon",
|
||||
data_schema=vol.Schema(
|
||||
{vol.Required(CONF_KAMEREON_ACCOUNT_ID): vol.In(accounts)}
|
||||
),
|
||||
)
|
15
homeassistant/components/renault/const.py
Normal file
15
homeassistant/components/renault/const.py
Normal file
@ -0,0 +1,15 @@
|
||||
"""Constants for the Renault component."""
|
||||
DOMAIN = "renault"
|
||||
|
||||
CONF_LOCALE = "locale"
|
||||
CONF_KAMEREON_ACCOUNT_ID = "kamereon_account_id"
|
||||
|
||||
DEFAULT_SCAN_INTERVAL = 300 # 5 minutes
|
||||
|
||||
PLATFORMS = [
|
||||
"sensor",
|
||||
]
|
||||
|
||||
DEVICE_CLASS_PLUG_STATE = "renault__plug_state"
|
||||
DEVICE_CLASS_CHARGE_STATE = "renault__charge_state"
|
||||
DEVICE_CLASS_CHARGE_MODE = "renault__charge_mode"
|
13
homeassistant/components/renault/manifest.json
Normal file
13
homeassistant/components/renault/manifest.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"domain": "renault",
|
||||
"name": "Renault",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/renault",
|
||||
"requirements": [
|
||||
"renault-api==0.1.4"
|
||||
],
|
||||
"codeowners": [
|
||||
"@epenet"
|
||||
],
|
||||
"iot_class": "cloud_polling"
|
||||
}
|
72
homeassistant/components/renault/renault_coordinator.py
Normal file
72
homeassistant/components/renault/renault_coordinator.py
Normal file
@ -0,0 +1,72 @@
|
||||
"""Proxy to handle account communication with Renault servers."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Callable, TypeVar
|
||||
|
||||
from renault_api.kamereon.exceptions import (
|
||||
AccessDeniedException,
|
||||
KamereonResponseException,
|
||||
NotSupportedException,
|
||||
)
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class RenaultDataUpdateCoordinator(DataUpdateCoordinator[T]):
|
||||
"""Handle vehicle communication with Renault servers."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
logger: logging.Logger,
|
||||
*,
|
||||
name: str,
|
||||
update_interval: timedelta,
|
||||
update_method: Callable[[], Awaitable[T]],
|
||||
) -> None:
|
||||
"""Initialise coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
logger,
|
||||
name=name,
|
||||
update_interval=update_interval,
|
||||
update_method=update_method,
|
||||
)
|
||||
self.access_denied = False
|
||||
self.not_supported = False
|
||||
|
||||
async def _async_update_data(self) -> T:
|
||||
"""Fetch the latest data from the source."""
|
||||
if self.update_method is None:
|
||||
raise NotImplementedError("Update method not implemented")
|
||||
try:
|
||||
return await self.update_method()
|
||||
except AccessDeniedException as err:
|
||||
# Disable because the account is not allowed to access this Renault endpoint.
|
||||
self.update_interval = None
|
||||
self.access_denied = True
|
||||
raise UpdateFailed(f"This endpoint is denied: {err}") from err
|
||||
|
||||
except NotSupportedException as err:
|
||||
# Disable because the vehicle does not support this Renault endpoint.
|
||||
self.update_interval = None
|
||||
self.not_supported = True
|
||||
raise UpdateFailed(f"This endpoint is not supported: {err}") from err
|
||||
|
||||
except KamereonResponseException as err:
|
||||
# Other Renault errors.
|
||||
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
||||
|
||||
async def async_config_entry_first_refresh(self) -> None:
|
||||
"""Refresh data for the first time when a config entry is setup.
|
||||
|
||||
Contrary to base implementation, we are not raising ConfigEntryNotReady
|
||||
but only updating the `access_denied` and `not_supported` flags.
|
||||
"""
|
||||
await self._async_refresh(log_failures=False, raise_on_auth_failed=True)
|
103
homeassistant/components/renault/renault_entities.py
Normal file
103
homeassistant/components/renault/renault_entities.py
Normal file
@ -0,0 +1,103 @@
|
||||
"""Base classes for Renault entities."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Generic, Optional, TypeVar
|
||||
|
||||
from renault_api.kamereon.enums import ChargeState, PlugState
|
||||
from renault_api.kamereon.models import (
|
||||
KamereonVehicleBatteryStatusData,
|
||||
KamereonVehicleChargeModeData,
|
||||
KamereonVehicleCockpitData,
|
||||
KamereonVehicleHvacStatusData,
|
||||
)
|
||||
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from .renault_vehicle import RenaultVehicleProxy
|
||||
|
||||
ATTR_LAST_UPDATE = "last_update"
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class RenaultDataEntity(Generic[T], CoordinatorEntity[Optional[T]], Entity):
|
||||
"""Implementation of a Renault entity with a data coordinator."""
|
||||
|
||||
def __init__(
|
||||
self, vehicle: RenaultVehicleProxy, entity_type: str, coordinator_key: str
|
||||
) -> None:
|
||||
"""Initialise entity."""
|
||||
super().__init__(vehicle.coordinators[coordinator_key])
|
||||
self.vehicle = vehicle
|
||||
self._entity_type = entity_type
|
||||
self._attr_device_info = self.vehicle.device_info
|
||||
self._attr_name = entity_type
|
||||
self._attr_unique_id = slugify(
|
||||
f"{self.vehicle.details.vin}-{self._entity_type}"
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
# Data can succeed, but be empty
|
||||
return super().available and self.coordinator.data is not None
|
||||
|
||||
@property
|
||||
def data(self) -> T | None:
|
||||
"""Return collected data."""
|
||||
return self.coordinator.data
|
||||
|
||||
|
||||
class RenaultBatteryDataEntity(RenaultDataEntity[KamereonVehicleBatteryStatusData]):
|
||||
"""Implementation of a Renault entity with battery coordinator."""
|
||||
|
||||
def __init__(self, vehicle: RenaultVehicleProxy, entity_type: str) -> None:
|
||||
"""Initialise entity."""
|
||||
super().__init__(vehicle, entity_type, "battery")
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the state attributes of this entity."""
|
||||
last_update = self.data.timestamp if self.data else None
|
||||
return {ATTR_LAST_UPDATE: last_update}
|
||||
|
||||
@property
|
||||
def is_charging(self) -> bool:
|
||||
"""Return charge state as boolean."""
|
||||
return (
|
||||
self.data is not None
|
||||
and self.data.get_charging_status() == ChargeState.CHARGE_IN_PROGRESS
|
||||
)
|
||||
|
||||
@property
|
||||
def is_plugged_in(self) -> bool:
|
||||
"""Return plug state as boolean."""
|
||||
return (
|
||||
self.data is not None and self.data.get_plug_status() == PlugState.PLUGGED
|
||||
)
|
||||
|
||||
|
||||
class RenaultChargeModeDataEntity(RenaultDataEntity[KamereonVehicleChargeModeData]):
|
||||
"""Implementation of a Renault entity with charge_mode coordinator."""
|
||||
|
||||
def __init__(self, vehicle: RenaultVehicleProxy, entity_type: str) -> None:
|
||||
"""Initialise entity."""
|
||||
super().__init__(vehicle, entity_type, "charge_mode")
|
||||
|
||||
|
||||
class RenaultCockpitDataEntity(RenaultDataEntity[KamereonVehicleCockpitData]):
|
||||
"""Implementation of a Renault entity with cockpit coordinator."""
|
||||
|
||||
def __init__(self, vehicle: RenaultVehicleProxy, entity_type: str) -> None:
|
||||
"""Initialise entity."""
|
||||
super().__init__(vehicle, entity_type, "cockpit")
|
||||
|
||||
|
||||
class RenaultHVACDataEntity(RenaultDataEntity[KamereonVehicleHvacStatusData]):
|
||||
"""Implementation of a Renault entity with hvac_status coordinator."""
|
||||
|
||||
def __init__(self, vehicle: RenaultVehicleProxy, entity_type: str) -> None:
|
||||
"""Initialise entity."""
|
||||
super().__init__(vehicle, entity_type, "hvac_status")
|
78
homeassistant/components/renault/renault_hub.py
Normal file
78
homeassistant/components/renault/renault_hub.py
Normal file
@ -0,0 +1,78 @@
|
||||
"""Proxy to handle account communication with Renault servers."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from renault_api.gigya.exceptions import InvalidCredentialsException
|
||||
from renault_api.renault_account import RenaultAccount
|
||||
from renault_api.renault_client import RenaultClient
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import CONF_KAMEREON_ACCOUNT_ID, DEFAULT_SCAN_INTERVAL
|
||||
from .renault_vehicle import RenaultVehicleProxy
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RenaultHub:
|
||||
"""Handle account communication with Renault servers."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, locale: str) -> None:
|
||||
"""Initialise proxy."""
|
||||
LOGGER.debug("Creating RenaultHub")
|
||||
self._hass = hass
|
||||
self._client = RenaultClient(
|
||||
websession=async_get_clientsession(self._hass), locale=locale
|
||||
)
|
||||
self._account: RenaultAccount | None = None
|
||||
self._vehicles: dict[str, RenaultVehicleProxy] = {}
|
||||
|
||||
async def attempt_login(self, username: str, password: str) -> bool:
|
||||
"""Attempt login to Renault servers."""
|
||||
try:
|
||||
await self._client.session.login(username, password)
|
||||
except InvalidCredentialsException as ex:
|
||||
LOGGER.error("Login to Renault failed: %s", ex.error_details)
|
||||
else:
|
||||
return True
|
||||
return False
|
||||
|
||||
async def async_initialise(self, config_entry: ConfigEntry) -> None:
|
||||
"""Set up proxy."""
|
||||
account_id: str = config_entry.data[CONF_KAMEREON_ACCOUNT_ID]
|
||||
scan_interval = timedelta(seconds=DEFAULT_SCAN_INTERVAL)
|
||||
|
||||
self._account = await self._client.get_api_account(account_id)
|
||||
vehicles = await self._account.get_vehicles()
|
||||
if vehicles.vehicleLinks:
|
||||
for vehicle_link in vehicles.vehicleLinks:
|
||||
if vehicle_link.vin and vehicle_link.vehicleDetails:
|
||||
# Generate vehicle proxy
|
||||
vehicle = RenaultVehicleProxy(
|
||||
hass=self._hass,
|
||||
vehicle=await self._account.get_api_vehicle(vehicle_link.vin),
|
||||
details=vehicle_link.vehicleDetails,
|
||||
scan_interval=scan_interval,
|
||||
)
|
||||
await vehicle.async_initialise()
|
||||
self._vehicles[vehicle_link.vin] = vehicle
|
||||
|
||||
async def get_account_ids(self) -> list[str]:
|
||||
"""Get Kamereon account ids."""
|
||||
accounts = []
|
||||
for account in await self._client.get_api_accounts():
|
||||
vehicles = await account.get_vehicles()
|
||||
|
||||
# Only add the account if it has linked vehicles.
|
||||
if vehicles.vehicleLinks:
|
||||
accounts.append(account.account_id)
|
||||
return accounts
|
||||
|
||||
@property
|
||||
def vehicles(self) -> dict[str, RenaultVehicleProxy]:
|
||||
"""Get list of vehicles."""
|
||||
return self._vehicles
|
146
homeassistant/components/renault/renault_vehicle.py
Normal file
146
homeassistant/components/renault/renault_vehicle.py
Normal file
@ -0,0 +1,146 @@
|
||||
"""Proxy to handle account communication with Renault servers."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
from renault_api.kamereon import models
|
||||
from renault_api.renault_vehicle import RenaultVehicle
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
|
||||
from .const import DOMAIN
|
||||
from .renault_coordinator import RenaultDataUpdateCoordinator
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RenaultVehicleProxy:
|
||||
"""Handle vehicle communication with Renault servers."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
vehicle: RenaultVehicle,
|
||||
details: models.KamereonVehicleDetails,
|
||||
scan_interval: timedelta,
|
||||
) -> None:
|
||||
"""Initialise vehicle proxy."""
|
||||
self.hass = hass
|
||||
self._vehicle = vehicle
|
||||
self._details = details
|
||||
self._device_info: DeviceInfo = {
|
||||
"identifiers": {(DOMAIN, cast(str, details.vin))},
|
||||
"manufacturer": (details.get_brand_label() or "").capitalize(),
|
||||
"model": (details.get_model_label() or "").capitalize(),
|
||||
"name": details.registrationNumber or "",
|
||||
"sw_version": details.get_model_code() or "",
|
||||
}
|
||||
self.coordinators: dict[str, RenaultDataUpdateCoordinator] = {}
|
||||
self.hvac_target_temperature = 21
|
||||
self._scan_interval = scan_interval
|
||||
|
||||
@property
|
||||
def details(self) -> models.KamereonVehicleDetails:
|
||||
"""Return the specs of the vehicle."""
|
||||
return self._details
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return a device description for device registry."""
|
||||
return self._device_info
|
||||
|
||||
async def async_initialise(self) -> None:
|
||||
"""Load available sensors."""
|
||||
if await self.endpoint_available("cockpit"):
|
||||
self.coordinators["cockpit"] = RenaultDataUpdateCoordinator(
|
||||
self.hass,
|
||||
LOGGER,
|
||||
# Name of the data. For logging purposes.
|
||||
name=f"{self.details.vin} cockpit",
|
||||
update_method=self.get_cockpit,
|
||||
# Polling interval. Will only be polled if there are subscribers.
|
||||
update_interval=self._scan_interval,
|
||||
)
|
||||
if await self.endpoint_available("hvac-status"):
|
||||
self.coordinators["hvac_status"] = RenaultDataUpdateCoordinator(
|
||||
self.hass,
|
||||
LOGGER,
|
||||
# Name of the data. For logging purposes.
|
||||
name=f"{self.details.vin} hvac_status",
|
||||
update_method=self.get_hvac_status,
|
||||
# Polling interval. Will only be polled if there are subscribers.
|
||||
update_interval=self._scan_interval,
|
||||
)
|
||||
if self.details.uses_electricity():
|
||||
if await self.endpoint_available("battery-status"):
|
||||
self.coordinators["battery"] = RenaultDataUpdateCoordinator(
|
||||
self.hass,
|
||||
LOGGER,
|
||||
# Name of the data. For logging purposes.
|
||||
name=f"{self.details.vin} battery",
|
||||
update_method=self.get_battery_status,
|
||||
# Polling interval. Will only be polled if there are subscribers.
|
||||
update_interval=self._scan_interval,
|
||||
)
|
||||
if await self.endpoint_available("charge-mode"):
|
||||
self.coordinators["charge_mode"] = RenaultDataUpdateCoordinator(
|
||||
self.hass,
|
||||
LOGGER,
|
||||
# Name of the data. For logging purposes.
|
||||
name=f"{self.details.vin} charge_mode",
|
||||
update_method=self.get_charge_mode,
|
||||
# Polling interval. Will only be polled if there are subscribers.
|
||||
update_interval=self._scan_interval,
|
||||
)
|
||||
# Check all coordinators
|
||||
await asyncio.gather(
|
||||
*(
|
||||
coordinator.async_config_entry_first_refresh()
|
||||
for coordinator in self.coordinators.values()
|
||||
)
|
||||
)
|
||||
for key in list(self.coordinators):
|
||||
# list() to avoid Runtime iteration error
|
||||
coordinator = self.coordinators[key]
|
||||
if coordinator.not_supported:
|
||||
# Remove endpoint as it is not supported for this vehicle.
|
||||
LOGGER.error(
|
||||
"Ignoring endpoint %s as it is not supported for this vehicle: %s",
|
||||
coordinator.name,
|
||||
coordinator.last_exception,
|
||||
)
|
||||
del self.coordinators[key]
|
||||
elif coordinator.access_denied:
|
||||
# Remove endpoint as it is denied for this vehicle.
|
||||
LOGGER.error(
|
||||
"Ignoring endpoint %s as it is denied for this vehicle: %s",
|
||||
coordinator.name,
|
||||
coordinator.last_exception,
|
||||
)
|
||||
del self.coordinators[key]
|
||||
|
||||
async def endpoint_available(self, endpoint: str) -> bool:
|
||||
"""Ensure the endpoint is available to avoid unnecessary queries."""
|
||||
return await self._vehicle.supports_endpoint(
|
||||
endpoint
|
||||
) and await self._vehicle.has_contract_for_endpoint(endpoint)
|
||||
|
||||
async def get_battery_status(self) -> models.KamereonVehicleBatteryStatusData:
|
||||
"""Get battery status information from vehicle."""
|
||||
return await self._vehicle.get_battery_status()
|
||||
|
||||
async def get_charge_mode(self) -> models.KamereonVehicleChargeModeData:
|
||||
"""Get charge mode information from vehicle."""
|
||||
return await self._vehicle.get_charge_mode()
|
||||
|
||||
async def get_cockpit(self) -> models.KamereonVehicleCockpitData:
|
||||
"""Get cockpit information from vehicle."""
|
||||
return await self._vehicle.get_cockpit()
|
||||
|
||||
async def get_hvac_status(self) -> models.KamereonVehicleHvacStatusData:
|
||||
"""Get hvac status information from vehicle."""
|
||||
return await self._vehicle.get_hvac_status()
|
277
homeassistant/components/renault/sensor.py
Normal file
277
homeassistant/components/renault/sensor.py
Normal file
@ -0,0 +1,277 @@
|
||||
"""Support for Renault sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
DEVICE_CLASS_BATTERY,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
LENGTH_KILOMETERS,
|
||||
PERCENTAGE,
|
||||
POWER_KILO_WATT,
|
||||
TEMP_CELSIUS,
|
||||
TIME_MINUTES,
|
||||
VOLUME_LITERS,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.icon import icon_for_battery_level
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from .const import (
|
||||
DEVICE_CLASS_CHARGE_MODE,
|
||||
DEVICE_CLASS_CHARGE_STATE,
|
||||
DEVICE_CLASS_PLUG_STATE,
|
||||
DOMAIN,
|
||||
)
|
||||
from .renault_entities import (
|
||||
RenaultBatteryDataEntity,
|
||||
RenaultChargeModeDataEntity,
|
||||
RenaultCockpitDataEntity,
|
||||
RenaultDataEntity,
|
||||
RenaultHVACDataEntity,
|
||||
)
|
||||
from .renault_hub import RenaultHub
|
||||
from .renault_vehicle import RenaultVehicleProxy
|
||||
|
||||
ATTR_BATTERY_AVAILABLE_ENERGY = "battery_available_energy"
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Renault entities from config entry."""
|
||||
proxy: RenaultHub = hass.data[DOMAIN][config_entry.unique_id]
|
||||
entities = await get_entities(proxy)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
async def get_entities(proxy: RenaultHub) -> list[RenaultDataEntity]:
|
||||
"""Create Renault entities for all vehicles."""
|
||||
entities = []
|
||||
for vehicle in proxy.vehicles.values():
|
||||
entities.extend(await get_vehicle_entities(vehicle))
|
||||
return entities
|
||||
|
||||
|
||||
async def get_vehicle_entities(vehicle: RenaultVehicleProxy) -> list[RenaultDataEntity]:
|
||||
"""Create Renault entities for single vehicle."""
|
||||
entities: list[RenaultDataEntity] = []
|
||||
if "cockpit" in vehicle.coordinators:
|
||||
entities.append(RenaultMileageSensor(vehicle, "Mileage"))
|
||||
if vehicle.details.uses_fuel():
|
||||
entities.append(RenaultFuelAutonomySensor(vehicle, "Fuel Autonomy"))
|
||||
entities.append(RenaultFuelQuantitySensor(vehicle, "Fuel Quantity"))
|
||||
if "hvac_status" in vehicle.coordinators:
|
||||
entities.append(RenaultOutsideTemperatureSensor(vehicle, "Outside Temperature"))
|
||||
if "battery" in vehicle.coordinators:
|
||||
entities.append(RenaultBatteryLevelSensor(vehicle, "Battery Level"))
|
||||
entities.append(RenaultChargeStateSensor(vehicle, "Charge State"))
|
||||
entities.append(
|
||||
RenaultChargingRemainingTimeSensor(vehicle, "Charging Remaining Time")
|
||||
)
|
||||
entities.append(RenaultChargingPowerSensor(vehicle, "Charging Power"))
|
||||
entities.append(RenaultPlugStateSensor(vehicle, "Plug State"))
|
||||
entities.append(RenaultBatteryAutonomySensor(vehicle, "Battery Autonomy"))
|
||||
entities.append(RenaultBatteryTemperatureSensor(vehicle, "Battery Temperature"))
|
||||
if "charge_mode" in vehicle.coordinators:
|
||||
entities.append(RenaultChargeModeSensor(vehicle, "Charge Mode"))
|
||||
return entities
|
||||
|
||||
|
||||
class RenaultBatteryAutonomySensor(RenaultBatteryDataEntity, SensorEntity):
|
||||
"""Battery autonomy sensor."""
|
||||
|
||||
_attr_icon = "mdi:ev-station"
|
||||
_attr_unit_of_measurement = LENGTH_KILOMETERS
|
||||
|
||||
@property
|
||||
def state(self) -> int | None:
|
||||
"""Return the state of this entity."""
|
||||
return self.data.batteryAutonomy if self.data else None
|
||||
|
||||
|
||||
class RenaultBatteryLevelSensor(RenaultBatteryDataEntity, SensorEntity):
|
||||
"""Battery Level sensor."""
|
||||
|
||||
_attr_device_class = DEVICE_CLASS_BATTERY
|
||||
_attr_unit_of_measurement = PERCENTAGE
|
||||
|
||||
@property
|
||||
def state(self) -> int | None:
|
||||
"""Return the state of this entity."""
|
||||
return self.data.batteryLevel if self.data else None
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
"""Icon handling."""
|
||||
return icon_for_battery_level(
|
||||
battery_level=self.state, charging=self.is_charging
|
||||
)
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the state attributes of this entity."""
|
||||
attrs = super().extra_state_attributes
|
||||
attrs[ATTR_BATTERY_AVAILABLE_ENERGY] = (
|
||||
self.data.batteryAvailableEnergy if self.data else None
|
||||
)
|
||||
return attrs
|
||||
|
||||
|
||||
class RenaultBatteryTemperatureSensor(RenaultBatteryDataEntity, SensorEntity):
|
||||
"""Battery Temperature sensor."""
|
||||
|
||||
_attr_device_class = DEVICE_CLASS_TEMPERATURE
|
||||
_attr_unit_of_measurement = TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def state(self) -> int | None:
|
||||
"""Return the state of this entity."""
|
||||
return self.data.batteryTemperature if self.data else None
|
||||
|
||||
|
||||
class RenaultChargeModeSensor(RenaultChargeModeDataEntity, SensorEntity):
|
||||
"""Charge Mode sensor."""
|
||||
|
||||
_attr_device_class = DEVICE_CLASS_CHARGE_MODE
|
||||
|
||||
@property
|
||||
def state(self) -> str | None:
|
||||
"""Return the state of this entity."""
|
||||
return self.data.chargeMode if self.data else None
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
"""Icon handling."""
|
||||
if self.data and self.data.chargeMode == "schedule_mode":
|
||||
return "mdi:calendar-clock"
|
||||
return "mdi:calendar-remove"
|
||||
|
||||
|
||||
class RenaultChargeStateSensor(RenaultBatteryDataEntity, SensorEntity):
|
||||
"""Charge State sensor."""
|
||||
|
||||
_attr_device_class = DEVICE_CLASS_CHARGE_STATE
|
||||
|
||||
@property
|
||||
def state(self) -> str | None:
|
||||
"""Return the state of this entity."""
|
||||
charging_status = self.data.get_charging_status() if self.data else None
|
||||
return slugify(charging_status.name) if charging_status is not None else None
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
"""Icon handling."""
|
||||
return "mdi:flash" if self.is_charging else "mdi:flash-off"
|
||||
|
||||
|
||||
class RenaultChargingRemainingTimeSensor(RenaultBatteryDataEntity, SensorEntity):
|
||||
"""Charging Remaining Time sensor."""
|
||||
|
||||
_attr_icon = "mdi:timer"
|
||||
_attr_unit_of_measurement = TIME_MINUTES
|
||||
|
||||
@property
|
||||
def state(self) -> int | None:
|
||||
"""Return the state of this entity."""
|
||||
return self.data.chargingRemainingTime if self.data else None
|
||||
|
||||
|
||||
class RenaultChargingPowerSensor(RenaultBatteryDataEntity, SensorEntity):
|
||||
"""Charging Power sensor."""
|
||||
|
||||
_attr_device_class = DEVICE_CLASS_ENERGY
|
||||
_attr_unit_of_measurement = POWER_KILO_WATT
|
||||
|
||||
@property
|
||||
def state(self) -> float | None:
|
||||
"""Return the state of this entity."""
|
||||
if not self.data or self.data.chargingInstantaneousPower is None:
|
||||
return None
|
||||
if self.vehicle.details.reports_charging_power_in_watts():
|
||||
# Need to convert to kilowatts
|
||||
return self.data.chargingInstantaneousPower / 1000
|
||||
return self.data.chargingInstantaneousPower
|
||||
|
||||
|
||||
class RenaultFuelAutonomySensor(RenaultCockpitDataEntity, SensorEntity):
|
||||
"""Fuel autonomy sensor."""
|
||||
|
||||
_attr_icon = "mdi:gas-station"
|
||||
_attr_unit_of_measurement = LENGTH_KILOMETERS
|
||||
|
||||
@property
|
||||
def state(self) -> int | None:
|
||||
"""Return the state of this entity."""
|
||||
return (
|
||||
round(self.data.fuelAutonomy)
|
||||
if self.data and self.data.fuelAutonomy is not None
|
||||
else None
|
||||
)
|
||||
|
||||
|
||||
class RenaultFuelQuantitySensor(RenaultCockpitDataEntity, SensorEntity):
|
||||
"""Fuel quantity sensor."""
|
||||
|
||||
_attr_icon = "mdi:fuel"
|
||||
_attr_unit_of_measurement = VOLUME_LITERS
|
||||
|
||||
@property
|
||||
def state(self) -> int | None:
|
||||
"""Return the state of this entity."""
|
||||
return (
|
||||
round(self.data.fuelQuantity)
|
||||
if self.data and self.data.fuelQuantity is not None
|
||||
else None
|
||||
)
|
||||
|
||||
|
||||
class RenaultMileageSensor(RenaultCockpitDataEntity, SensorEntity):
|
||||
"""Mileage sensor."""
|
||||
|
||||
_attr_icon = "mdi:sign-direction"
|
||||
_attr_unit_of_measurement = LENGTH_KILOMETERS
|
||||
|
||||
@property
|
||||
def state(self) -> int | None:
|
||||
"""Return the state of this entity."""
|
||||
return (
|
||||
round(self.data.totalMileage)
|
||||
if self.data and self.data.totalMileage is not None
|
||||
else None
|
||||
)
|
||||
|
||||
|
||||
class RenaultOutsideTemperatureSensor(RenaultHVACDataEntity, SensorEntity):
|
||||
"""HVAC Outside Temperature sensor."""
|
||||
|
||||
_attr_device_class = DEVICE_CLASS_TEMPERATURE
|
||||
_attr_unit_of_measurement = TEMP_CELSIUS
|
||||
|
||||
@property
|
||||
def state(self) -> float | None:
|
||||
"""Return the state of this entity."""
|
||||
return self.data.externalTemperature if self.data else None
|
||||
|
||||
|
||||
class RenaultPlugStateSensor(RenaultBatteryDataEntity, SensorEntity):
|
||||
"""Plug State sensor."""
|
||||
|
||||
_attr_device_class = DEVICE_CLASS_PLUG_STATE
|
||||
|
||||
@property
|
||||
def state(self) -> str | None:
|
||||
"""Return the state of this entity."""
|
||||
plug_status = self.data.get_plug_status() if self.data else None
|
||||
return slugify(plug_status.name) if plug_status is not None else None
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
"""Icon handling."""
|
||||
return "mdi:power-plug" if self.is_plugged_in else "mdi:power-plug-off"
|
27
homeassistant/components/renault/strings.json
Normal file
27
homeassistant/components/renault/strings.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||
"kamereon_no_account": "Unable to find Kamereon account."
|
||||
},
|
||||
"error": {
|
||||
"invalid_credentials": "[%key:common::config_flow::error::invalid_auth%]"
|
||||
},
|
||||
"step": {
|
||||
"kamereon": {
|
||||
"data": {
|
||||
"kamereon_account_id": "Kamereon account id"
|
||||
},
|
||||
"title": "Select Kamereon account id"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"locale": "Locale",
|
||||
"username": "[%key:common::config_flow::data::email%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"title": "Set Renault credentials"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
27
homeassistant/components/renault/translations/en.json
Normal file
27
homeassistant/components/renault/translations/en.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Account already configured",
|
||||
"kamereon_no_account": "Unable to find Kamereon account."
|
||||
},
|
||||
"error": {
|
||||
"invalid_credentials": "Invalid credentials."
|
||||
},
|
||||
"step": {
|
||||
"kamereon": {
|
||||
"data": {
|
||||
"kamereon_account_id": "Kamereon account id"
|
||||
},
|
||||
"title": "Select Kamereon account id"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"locale": "Locale",
|
||||
"username": "Email",
|
||||
"password": "Password"
|
||||
},
|
||||
"title": "Set Renault credentials"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -216,6 +216,7 @@ FLOWS = [
|
||||
"rachio",
|
||||
"rainmachine",
|
||||
"recollect_waste",
|
||||
"renault",
|
||||
"rfxtrx",
|
||||
"ring",
|
||||
"risco",
|
||||
|
11
mypy.ini
11
mypy.ini
@ -902,6 +902,17 @@ no_implicit_optional = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.renault.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
disallow_untyped_calls = true
|
||||
disallow_untyped_decorators = true
|
||||
disallow_untyped_defs = true
|
||||
no_implicit_optional = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.rituals_perfume_genie.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
|
@ -2015,6 +2015,9 @@ raspyrfm-client==1.2.8
|
||||
# homeassistant.components.rainmachine
|
||||
regenmaschine==3.1.5
|
||||
|
||||
# homeassistant.components.renault
|
||||
renault-api==0.1.4
|
||||
|
||||
# homeassistant.components.python_script
|
||||
restrictedpython==5.1
|
||||
|
||||
|
@ -1109,6 +1109,9 @@ rachiopy==1.0.3
|
||||
# homeassistant.components.rainmachine
|
||||
regenmaschine==3.1.5
|
||||
|
||||
# homeassistant.components.renault
|
||||
renault-api==0.1.4
|
||||
|
||||
# homeassistant.components.python_script
|
||||
restrictedpython==5.1
|
||||
|
||||
|
159
tests/components/renault/__init__.py
Normal file
159
tests/components/renault/__init__.py
Normal file
@ -0,0 +1,159 @@
|
||||
"""Tests for the Renault integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
from renault_api.kamereon import models, schemas
|
||||
from renault_api.renault_vehicle import RenaultVehicle
|
||||
|
||||
from homeassistant.components.renault.const import (
|
||||
CONF_KAMEREON_ACCOUNT_ID,
|
||||
CONF_LOCALE,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.components.renault.renault_vehicle import RenaultVehicleProxy
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
|
||||
from .const import MOCK_VEHICLES
|
||||
|
||||
from tests.common import MockConfigEntry, load_fixture
|
||||
|
||||
|
||||
async def setup_renault_integration(hass: HomeAssistant):
|
||||
"""Create the Renault integration."""
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
source="user",
|
||||
data={
|
||||
CONF_LOCALE: "fr_FR",
|
||||
CONF_USERNAME: "email@test.com",
|
||||
CONF_PASSWORD: "test",
|
||||
CONF_KAMEREON_ACCOUNT_ID: "account_id_2",
|
||||
},
|
||||
unique_id="account_id_2",
|
||||
options={},
|
||||
entry_id="1",
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.renault.RenaultHub.attempt_login", return_value=True
|
||||
), patch("homeassistant.components.renault.RenaultHub.async_initialise"):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return config_entry
|
||||
|
||||
|
||||
def get_fixtures(vehicle_type: str) -> dict[str, Any]:
|
||||
"""Create a vehicle proxy for testing."""
|
||||
mock_vehicle = MOCK_VEHICLES[vehicle_type]
|
||||
return {
|
||||
"battery_status": schemas.KamereonVehicleDataResponseSchema.loads(
|
||||
load_fixture(f"renault/{mock_vehicle['endpoints']['battery_status']}")
|
||||
if "battery_status" in mock_vehicle["endpoints"]
|
||||
else "{}"
|
||||
).get_attributes(schemas.KamereonVehicleBatteryStatusDataSchema),
|
||||
"charge_mode": schemas.KamereonVehicleDataResponseSchema.loads(
|
||||
load_fixture(f"renault/{mock_vehicle['endpoints']['charge_mode']}")
|
||||
if "charge_mode" in mock_vehicle["endpoints"]
|
||||
else "{}"
|
||||
).get_attributes(schemas.KamereonVehicleChargeModeDataSchema),
|
||||
"cockpit": schemas.KamereonVehicleDataResponseSchema.loads(
|
||||
load_fixture(f"renault/{mock_vehicle['endpoints']['cockpit']}")
|
||||
if "cockpit" in mock_vehicle["endpoints"]
|
||||
else "{}"
|
||||
).get_attributes(schemas.KamereonVehicleCockpitDataSchema),
|
||||
"hvac_status": schemas.KamereonVehicleDataResponseSchema.loads(
|
||||
load_fixture(f"renault/{mock_vehicle['endpoints']['hvac_status']}")
|
||||
if "hvac_status" in mock_vehicle["endpoints"]
|
||||
else "{}"
|
||||
).get_attributes(schemas.KamereonVehicleHvacStatusDataSchema),
|
||||
}
|
||||
|
||||
|
||||
async def create_vehicle_proxy(
|
||||
hass: HomeAssistant, vehicle_type: str
|
||||
) -> RenaultVehicleProxy:
|
||||
"""Create a vehicle proxy for testing."""
|
||||
mock_vehicle = MOCK_VEHICLES[vehicle_type]
|
||||
mock_fixtures = get_fixtures(vehicle_type)
|
||||
|
||||
vehicles_response: models.KamereonVehiclesResponse = (
|
||||
schemas.KamereonVehiclesResponseSchema.loads(
|
||||
load_fixture(f"renault/vehicle_{vehicle_type}.json")
|
||||
)
|
||||
)
|
||||
vehicle_details = vehicles_response.vehicleLinks[0].vehicleDetails
|
||||
vehicle = RenaultVehicle(
|
||||
vehicles_response.accountId,
|
||||
vehicle_details.vin,
|
||||
websession=aiohttp_client.async_get_clientsession(hass),
|
||||
)
|
||||
|
||||
vehicle_proxy = RenaultVehicleProxy(
|
||||
hass, vehicle, vehicle_details, timedelta(seconds=300)
|
||||
)
|
||||
with patch(
|
||||
"homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.endpoint_available",
|
||||
side_effect=mock_vehicle["endpoints_available"],
|
||||
), patch(
|
||||
"homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_battery_status",
|
||||
return_value=mock_fixtures["battery_status"],
|
||||
), patch(
|
||||
"homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_charge_mode",
|
||||
return_value=mock_fixtures["charge_mode"],
|
||||
), patch(
|
||||
"homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_cockpit",
|
||||
return_value=mock_fixtures["cockpit"],
|
||||
), patch(
|
||||
"homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_hvac_status",
|
||||
return_value=mock_fixtures["hvac_status"],
|
||||
):
|
||||
await vehicle_proxy.async_initialise()
|
||||
return vehicle_proxy
|
||||
|
||||
|
||||
async def create_vehicle_proxy_with_side_effect(
|
||||
hass: HomeAssistant, vehicle_type: str, side_effect: Any
|
||||
) -> RenaultVehicleProxy:
|
||||
"""Create a vehicle proxy for testing unavailable entities."""
|
||||
mock_vehicle = MOCK_VEHICLES[vehicle_type]
|
||||
|
||||
vehicles_response: models.KamereonVehiclesResponse = (
|
||||
schemas.KamereonVehiclesResponseSchema.loads(
|
||||
load_fixture(f"renault/vehicle_{vehicle_type}.json")
|
||||
)
|
||||
)
|
||||
vehicle_details = vehicles_response.vehicleLinks[0].vehicleDetails
|
||||
vehicle = RenaultVehicle(
|
||||
vehicles_response.accountId,
|
||||
vehicle_details.vin,
|
||||
websession=aiohttp_client.async_get_clientsession(hass),
|
||||
)
|
||||
|
||||
vehicle_proxy = RenaultVehicleProxy(
|
||||
hass, vehicle, vehicle_details, timedelta(seconds=300)
|
||||
)
|
||||
with patch(
|
||||
"homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.endpoint_available",
|
||||
side_effect=mock_vehicle["endpoints_available"],
|
||||
), patch(
|
||||
"homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_battery_status",
|
||||
side_effect=side_effect,
|
||||
), patch(
|
||||
"homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_charge_mode",
|
||||
side_effect=side_effect,
|
||||
), patch(
|
||||
"homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_cockpit",
|
||||
side_effect=side_effect,
|
||||
), patch(
|
||||
"homeassistant.components.renault.renault_vehicle.RenaultVehicleProxy.get_hvac_status",
|
||||
side_effect=side_effect,
|
||||
):
|
||||
await vehicle_proxy.async_initialise()
|
||||
return vehicle_proxy
|
328
tests/components/renault/const.py
Normal file
328
tests/components/renault/const.py
Normal file
@ -0,0 +1,328 @@
|
||||
"""Constants for the Renault integration tests."""
|
||||
from homeassistant.components.renault.const import (
|
||||
CONF_KAMEREON_ACCOUNT_ID,
|
||||
CONF_LOCALE,
|
||||
DEVICE_CLASS_CHARGE_MODE,
|
||||
DEVICE_CLASS_CHARGE_STATE,
|
||||
DEVICE_CLASS_PLUG_STATE,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
DEVICE_CLASS_BATTERY,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
LENGTH_KILOMETERS,
|
||||
PERCENTAGE,
|
||||
POWER_KILO_WATT,
|
||||
STATE_UNKNOWN,
|
||||
TEMP_CELSIUS,
|
||||
TIME_MINUTES,
|
||||
VOLUME_LITERS,
|
||||
)
|
||||
|
||||
# Mock config data to be used across multiple tests
|
||||
MOCK_CONFIG = {
|
||||
CONF_USERNAME: "email@test.com",
|
||||
CONF_PASSWORD: "test",
|
||||
CONF_KAMEREON_ACCOUNT_ID: "account_id_1",
|
||||
CONF_LOCALE: "fr_FR",
|
||||
}
|
||||
|
||||
MOCK_VEHICLES = {
|
||||
"zoe_40": {
|
||||
"expected_device": {
|
||||
"identifiers": {(DOMAIN, "VF1AAAAA555777999")},
|
||||
"manufacturer": "Renault",
|
||||
"model": "Zoe",
|
||||
"name": "REG-NUMBER",
|
||||
"sw_version": "X101VE",
|
||||
},
|
||||
"endpoints_available": [
|
||||
True, # cockpit
|
||||
True, # hvac-status
|
||||
True, # battery-status
|
||||
True, # charge-mode
|
||||
],
|
||||
"endpoints": {
|
||||
"battery_status": "battery_status_charging.json",
|
||||
"charge_mode": "charge_mode_always.json",
|
||||
"cockpit": "cockpit_ev.json",
|
||||
"hvac_status": "hvac_status.json",
|
||||
},
|
||||
SENSOR_DOMAIN: [
|
||||
{
|
||||
"entity_id": "sensor.battery_autonomy",
|
||||
"unique_id": "vf1aaaaa555777999_battery_autonomy",
|
||||
"result": "141",
|
||||
"unit": LENGTH_KILOMETERS,
|
||||
},
|
||||
{
|
||||
"entity_id": "sensor.battery_level",
|
||||
"unique_id": "vf1aaaaa555777999_battery_level",
|
||||
"result": "60",
|
||||
"unit": PERCENTAGE,
|
||||
"class": DEVICE_CLASS_BATTERY,
|
||||
},
|
||||
{
|
||||
"entity_id": "sensor.battery_temperature",
|
||||
"unique_id": "vf1aaaaa555777999_battery_temperature",
|
||||
"result": "20",
|
||||
"unit": TEMP_CELSIUS,
|
||||
"class": DEVICE_CLASS_TEMPERATURE,
|
||||
},
|
||||
{
|
||||
"entity_id": "sensor.charge_mode",
|
||||
"unique_id": "vf1aaaaa555777999_charge_mode",
|
||||
"result": "always",
|
||||
"class": DEVICE_CLASS_CHARGE_MODE,
|
||||
},
|
||||
{
|
||||
"entity_id": "sensor.charge_state",
|
||||
"unique_id": "vf1aaaaa555777999_charge_state",
|
||||
"result": "charge_in_progress",
|
||||
"class": DEVICE_CLASS_CHARGE_STATE,
|
||||
},
|
||||
{
|
||||
"entity_id": "sensor.charging_power",
|
||||
"unique_id": "vf1aaaaa555777999_charging_power",
|
||||
"result": "0.027",
|
||||
"unit": POWER_KILO_WATT,
|
||||
"class": DEVICE_CLASS_ENERGY,
|
||||
},
|
||||
{
|
||||
"entity_id": "sensor.charging_remaining_time",
|
||||
"unique_id": "vf1aaaaa555777999_charging_remaining_time",
|
||||
"result": "145",
|
||||
"unit": TIME_MINUTES,
|
||||
},
|
||||
{
|
||||
"entity_id": "sensor.mileage",
|
||||
"unique_id": "vf1aaaaa555777999_mileage",
|
||||
"result": "49114",
|
||||
"unit": LENGTH_KILOMETERS,
|
||||
},
|
||||
{
|
||||
"entity_id": "sensor.outside_temperature",
|
||||
"unique_id": "vf1aaaaa555777999_outside_temperature",
|
||||
"result": "8.0",
|
||||
"unit": TEMP_CELSIUS,
|
||||
"class": DEVICE_CLASS_TEMPERATURE,
|
||||
},
|
||||
{
|
||||
"entity_id": "sensor.plug_state",
|
||||
"unique_id": "vf1aaaaa555777999_plug_state",
|
||||
"result": "plugged",
|
||||
"class": DEVICE_CLASS_PLUG_STATE,
|
||||
},
|
||||
],
|
||||
},
|
||||
"zoe_50": {
|
||||
"expected_device": {
|
||||
"identifiers": {(DOMAIN, "VF1AAAAA555777999")},
|
||||
"manufacturer": "Renault",
|
||||
"model": "Zoe",
|
||||
"name": "REG-NUMBER",
|
||||
"sw_version": "X102VE",
|
||||
},
|
||||
"endpoints_available": [
|
||||
True, # cockpit
|
||||
False, # hvac-status
|
||||
True, # battery-status
|
||||
True, # charge-mode
|
||||
],
|
||||
"endpoints": {
|
||||
"battery_status": "battery_status_not_charging.json",
|
||||
"charge_mode": "charge_mode_schedule.json",
|
||||
"cockpit": "cockpit_ev.json",
|
||||
},
|
||||
SENSOR_DOMAIN: [
|
||||
{
|
||||
"entity_id": "sensor.battery_autonomy",
|
||||
"unique_id": "vf1aaaaa555777999_battery_autonomy",
|
||||
"result": "128",
|
||||
"unit": LENGTH_KILOMETERS,
|
||||
},
|
||||
{
|
||||
"entity_id": "sensor.battery_level",
|
||||
"unique_id": "vf1aaaaa555777999_battery_level",
|
||||
"result": "50",
|
||||
"unit": PERCENTAGE,
|
||||
"class": DEVICE_CLASS_BATTERY,
|
||||
},
|
||||
{
|
||||
"entity_id": "sensor.battery_temperature",
|
||||
"unique_id": "vf1aaaaa555777999_battery_temperature",
|
||||
"result": STATE_UNKNOWN,
|
||||
"unit": TEMP_CELSIUS,
|
||||
"class": DEVICE_CLASS_TEMPERATURE,
|
||||
},
|
||||
{
|
||||
"entity_id": "sensor.charge_mode",
|
||||
"unique_id": "vf1aaaaa555777999_charge_mode",
|
||||
"result": "schedule_mode",
|
||||
"class": DEVICE_CLASS_CHARGE_MODE,
|
||||
},
|
||||
{
|
||||
"entity_id": "sensor.charge_state",
|
||||
"unique_id": "vf1aaaaa555777999_charge_state",
|
||||
"result": "charge_error",
|
||||
"class": DEVICE_CLASS_CHARGE_STATE,
|
||||
},
|
||||
{
|
||||
"entity_id": "sensor.charging_power",
|
||||
"unique_id": "vf1aaaaa555777999_charging_power",
|
||||
"result": STATE_UNKNOWN,
|
||||
"unit": POWER_KILO_WATT,
|
||||
"class": DEVICE_CLASS_ENERGY,
|
||||
},
|
||||
{
|
||||
"entity_id": "sensor.charging_remaining_time",
|
||||
"unique_id": "vf1aaaaa555777999_charging_remaining_time",
|
||||
"result": STATE_UNKNOWN,
|
||||
"unit": TIME_MINUTES,
|
||||
},
|
||||
{
|
||||
"entity_id": "sensor.mileage",
|
||||
"unique_id": "vf1aaaaa555777999_mileage",
|
||||
"result": "49114",
|
||||
"unit": LENGTH_KILOMETERS,
|
||||
},
|
||||
{
|
||||
"entity_id": "sensor.plug_state",
|
||||
"unique_id": "vf1aaaaa555777999_plug_state",
|
||||
"result": "unplugged",
|
||||
"class": DEVICE_CLASS_PLUG_STATE,
|
||||
},
|
||||
],
|
||||
},
|
||||
"captur_phev": {
|
||||
"expected_device": {
|
||||
"identifiers": {(DOMAIN, "VF1AAAAA555777123")},
|
||||
"manufacturer": "Renault",
|
||||
"model": "Captur ii",
|
||||
"name": "REG-NUMBER",
|
||||
"sw_version": "XJB1SU",
|
||||
},
|
||||
"endpoints_available": [
|
||||
True, # cockpit
|
||||
False, # hvac-status
|
||||
True, # battery-status
|
||||
True, # charge-mode
|
||||
],
|
||||
"endpoints": {
|
||||
"battery_status": "battery_status_charging.json",
|
||||
"charge_mode": "charge_mode_always.json",
|
||||
"cockpit": "cockpit_fuel.json",
|
||||
},
|
||||
SENSOR_DOMAIN: [
|
||||
{
|
||||
"entity_id": "sensor.battery_autonomy",
|
||||
"unique_id": "vf1aaaaa555777123_battery_autonomy",
|
||||
"result": "141",
|
||||
"unit": LENGTH_KILOMETERS,
|
||||
},
|
||||
{
|
||||
"entity_id": "sensor.battery_level",
|
||||
"unique_id": "vf1aaaaa555777123_battery_level",
|
||||
"result": "60",
|
||||
"unit": PERCENTAGE,
|
||||
"class": DEVICE_CLASS_BATTERY,
|
||||
},
|
||||
{
|
||||
"entity_id": "sensor.battery_temperature",
|
||||
"unique_id": "vf1aaaaa555777123_battery_temperature",
|
||||
"result": "20",
|
||||
"unit": TEMP_CELSIUS,
|
||||
"class": DEVICE_CLASS_TEMPERATURE,
|
||||
},
|
||||
{
|
||||
"entity_id": "sensor.charge_mode",
|
||||
"unique_id": "vf1aaaaa555777123_charge_mode",
|
||||
"result": "always",
|
||||
"class": DEVICE_CLASS_CHARGE_MODE,
|
||||
},
|
||||
{
|
||||
"entity_id": "sensor.charge_state",
|
||||
"unique_id": "vf1aaaaa555777123_charge_state",
|
||||
"result": "charge_in_progress",
|
||||
"class": DEVICE_CLASS_CHARGE_STATE,
|
||||
},
|
||||
{
|
||||
"entity_id": "sensor.charging_power",
|
||||
"unique_id": "vf1aaaaa555777123_charging_power",
|
||||
"result": "27.0",
|
||||
"unit": POWER_KILO_WATT,
|
||||
"class": DEVICE_CLASS_ENERGY,
|
||||
},
|
||||
{
|
||||
"entity_id": "sensor.charging_remaining_time",
|
||||
"unique_id": "vf1aaaaa555777123_charging_remaining_time",
|
||||
"result": "145",
|
||||
"unit": TIME_MINUTES,
|
||||
},
|
||||
{
|
||||
"entity_id": "sensor.fuel_autonomy",
|
||||
"unique_id": "vf1aaaaa555777123_fuel_autonomy",
|
||||
"result": "35",
|
||||
"unit": LENGTH_KILOMETERS,
|
||||
},
|
||||
{
|
||||
"entity_id": "sensor.fuel_quantity",
|
||||
"unique_id": "vf1aaaaa555777123_fuel_quantity",
|
||||
"result": "3",
|
||||
"unit": VOLUME_LITERS,
|
||||
},
|
||||
{
|
||||
"entity_id": "sensor.mileage",
|
||||
"unique_id": "vf1aaaaa555777123_mileage",
|
||||
"result": "5567",
|
||||
"unit": LENGTH_KILOMETERS,
|
||||
},
|
||||
{
|
||||
"entity_id": "sensor.plug_state",
|
||||
"unique_id": "vf1aaaaa555777123_plug_state",
|
||||
"result": "plugged",
|
||||
"class": DEVICE_CLASS_PLUG_STATE,
|
||||
},
|
||||
],
|
||||
},
|
||||
"captur_fuel": {
|
||||
"expected_device": {
|
||||
"identifiers": {(DOMAIN, "VF1AAAAA555777123")},
|
||||
"manufacturer": "Renault",
|
||||
"model": "Captur ii",
|
||||
"name": "REG-NUMBER",
|
||||
"sw_version": "XJB1SU",
|
||||
},
|
||||
"endpoints_available": [
|
||||
True, # cockpit
|
||||
False, # hvac-status
|
||||
# Ignore, # battery-status
|
||||
# Ignore, # charge-mode
|
||||
],
|
||||
"endpoints": {"cockpit": "cockpit_fuel.json"},
|
||||
SENSOR_DOMAIN: [
|
||||
{
|
||||
"entity_id": "sensor.fuel_autonomy",
|
||||
"unique_id": "vf1aaaaa555777123_fuel_autonomy",
|
||||
"result": "35",
|
||||
"unit": LENGTH_KILOMETERS,
|
||||
},
|
||||
{
|
||||
"entity_id": "sensor.fuel_quantity",
|
||||
"unique_id": "vf1aaaaa555777123_fuel_quantity",
|
||||
"result": "3",
|
||||
"unit": VOLUME_LITERS,
|
||||
},
|
||||
{
|
||||
"entity_id": "sensor.mileage",
|
||||
"unique_id": "vf1aaaaa555777123_mileage",
|
||||
"result": "5567",
|
||||
"unit": LENGTH_KILOMETERS,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
137
tests/components/renault/test_config_flow.py
Normal file
137
tests/components/renault/test_config_flow.py
Normal file
@ -0,0 +1,137 @@
|
||||
"""Test the Renault config flow."""
|
||||
from unittest.mock import AsyncMock, PropertyMock, patch
|
||||
|
||||
from renault_api.gigya.exceptions import InvalidCredentialsException
|
||||
from renault_api.kamereon import schemas
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant.components.renault.const import (
|
||||
CONF_KAMEREON_ACCOUNT_ID,
|
||||
CONF_LOCALE,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import load_fixture
|
||||
|
||||
|
||||
async def test_config_flow_single_account(hass: HomeAssistant):
|
||||
"""Test we get the form."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
# Failed credentials
|
||||
with patch(
|
||||
"renault_api.renault_session.RenaultSession.login",
|
||||
side_effect=InvalidCredentialsException(403042, "invalid loginID or password"),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_LOCALE: "fr_FR",
|
||||
CONF_USERNAME: "email@test.com",
|
||||
CONF_PASSWORD: "test",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["errors"] == {"base": "invalid_credentials"}
|
||||
|
||||
renault_account = AsyncMock()
|
||||
type(renault_account).account_id = PropertyMock(return_value="account_id_1")
|
||||
renault_account.get_vehicles.return_value = (
|
||||
schemas.KamereonVehiclesResponseSchema.loads(
|
||||
load_fixture("renault/vehicle_zoe_40.json")
|
||||
)
|
||||
)
|
||||
|
||||
# Account list single
|
||||
with patch("renault_api.renault_session.RenaultSession.login"), patch(
|
||||
"renault_api.renault_account.RenaultAccount.account_id", return_value="123"
|
||||
), patch(
|
||||
"renault_api.renault_client.RenaultClient.get_api_accounts",
|
||||
return_value=[renault_account],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_LOCALE: "fr_FR",
|
||||
CONF_USERNAME: "email@test.com",
|
||||
CONF_PASSWORD: "test",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == "account_id_1"
|
||||
assert result["data"][CONF_USERNAME] == "email@test.com"
|
||||
assert result["data"][CONF_PASSWORD] == "test"
|
||||
assert result["data"][CONF_KAMEREON_ACCOUNT_ID] == "account_id_1"
|
||||
assert result["data"][CONF_LOCALE] == "fr_FR"
|
||||
|
||||
|
||||
async def test_config_flow_no_account(hass: HomeAssistant):
|
||||
"""Test we get the form."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
# Account list empty
|
||||
with patch("renault_api.renault_session.RenaultSession.login"), patch(
|
||||
"homeassistant.components.renault.config_flow.RenaultHub.get_account_ids",
|
||||
return_value=[],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_LOCALE: "fr_FR",
|
||||
CONF_USERNAME: "email@test.com",
|
||||
CONF_PASSWORD: "test",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "kamereon_no_account"
|
||||
|
||||
|
||||
async def test_config_flow_multiple_accounts(hass: HomeAssistant):
|
||||
"""Test what happens if multiple Kamereon accounts are available."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
# Multiple accounts
|
||||
with patch("renault_api.renault_session.RenaultSession.login"), patch(
|
||||
"homeassistant.components.renault.config_flow.RenaultHub.get_account_ids",
|
||||
return_value=["account_id_1", "account_id_2"],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_LOCALE: "fr_FR",
|
||||
CONF_USERNAME: "email@test.com",
|
||||
CONF_PASSWORD: "test",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "kamereon"
|
||||
|
||||
# Account selected
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_KAMEREON_ACCOUNT_ID: "account_id_2"},
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == "account_id_2"
|
||||
assert result["data"][CONF_USERNAME] == "email@test.com"
|
||||
assert result["data"][CONF_PASSWORD] == "test"
|
||||
assert result["data"][CONF_KAMEREON_ACCOUNT_ID] == "account_id_2"
|
||||
assert result["data"][CONF_LOCALE] == "fr_FR"
|
85
tests/components/renault/test_init.py
Normal file
85
tests/components/renault/test_init.py
Normal file
@ -0,0 +1,85 @@
|
||||
"""Tests for Renault setup process."""
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import aiohttp
|
||||
import pytest
|
||||
from renault_api.gigya.exceptions import InvalidCredentialsException
|
||||
from renault_api.kamereon import schemas
|
||||
|
||||
from homeassistant.components.renault import (
|
||||
RenaultHub,
|
||||
async_setup_entry,
|
||||
async_unload_entry,
|
||||
)
|
||||
from homeassistant.components.renault.const import DOMAIN
|
||||
from homeassistant.components.renault.renault_vehicle import RenaultVehicleProxy
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .const import MOCK_CONFIG
|
||||
|
||||
from tests.common import MockConfigEntry, load_fixture
|
||||
|
||||
|
||||
async def test_setup_unload_and_reload_entry(hass):
|
||||
"""Test entry setup and unload."""
|
||||
# Create a mock entry so we don't have to go through config flow
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data=MOCK_CONFIG, entry_id="test", unique_id=123456
|
||||
)
|
||||
renault_account = AsyncMock()
|
||||
renault_account.get_vehicles.return_value = (
|
||||
schemas.KamereonVehiclesResponseSchema.loads(
|
||||
load_fixture("renault/vehicle_zoe_40.json")
|
||||
)
|
||||
)
|
||||
|
||||
with patch("renault_api.renault_session.RenaultSession.login"), patch(
|
||||
"renault_api.renault_client.RenaultClient.get_api_account",
|
||||
return_value=renault_account,
|
||||
):
|
||||
# Set up the entry and assert that the values set during setup are where we expect
|
||||
# them to be.
|
||||
assert await async_setup_entry(hass, config_entry)
|
||||
assert DOMAIN in hass.data and config_entry.unique_id in hass.data[DOMAIN]
|
||||
assert isinstance(hass.data[DOMAIN][config_entry.unique_id], RenaultHub)
|
||||
|
||||
renault_hub: RenaultHub = hass.data[DOMAIN][config_entry.unique_id]
|
||||
assert len(renault_hub.vehicles) == 1
|
||||
assert isinstance(
|
||||
renault_hub.vehicles["VF1AAAAA555777999"], RenaultVehicleProxy
|
||||
)
|
||||
|
||||
# Unload the entry and verify that the data has been removed
|
||||
assert await async_unload_entry(hass, config_entry)
|
||||
assert config_entry.unique_id not in hass.data[DOMAIN]
|
||||
|
||||
|
||||
async def test_setup_entry_bad_password(hass):
|
||||
"""Test entry setup and unload."""
|
||||
# Create a mock entry so we don't have to go through config flow
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data=MOCK_CONFIG, entry_id="test", unique_id=123456
|
||||
)
|
||||
|
||||
with patch(
|
||||
"renault_api.renault_session.RenaultSession.login",
|
||||
side_effect=InvalidCredentialsException(403042, "invalid loginID or password"),
|
||||
):
|
||||
# Set up the entry and assert that the values set during setup are where we expect
|
||||
# them to be.
|
||||
assert not await async_setup_entry(hass, config_entry)
|
||||
|
||||
|
||||
async def test_setup_entry_exception(hass):
|
||||
"""Test ConfigEntryNotReady when API raises an exception during entry setup."""
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, data=MOCK_CONFIG, entry_id="test", unique_id=123456
|
||||
)
|
||||
|
||||
# In this case we are testing the condition where async_setup_entry raises
|
||||
# ConfigEntryNotReady.
|
||||
with patch(
|
||||
"renault_api.renault_session.RenaultSession.login",
|
||||
side_effect=aiohttp.ClientConnectionError,
|
||||
), pytest.raises(ConfigEntryNotReady):
|
||||
assert await async_setup_entry(hass, config_entry)
|
212
tests/components/renault/test_sensor.py
Normal file
212
tests/components/renault/test_sensor.py
Normal file
@ -0,0 +1,212 @@
|
||||
"""Tests for Renault sensors."""
|
||||
from unittest.mock import PropertyMock, patch
|
||||
|
||||
import pytest
|
||||
from renault_api.kamereon import exceptions
|
||||
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from . import (
|
||||
create_vehicle_proxy,
|
||||
create_vehicle_proxy_with_side_effect,
|
||||
setup_renault_integration,
|
||||
)
|
||||
from .const import MOCK_VEHICLES
|
||||
|
||||
from tests.common import mock_device_registry, mock_registry
|
||||
|
||||
|
||||
@pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys())
|
||||
async def test_sensors(hass, vehicle_type):
|
||||
"""Test for Renault sensors."""
|
||||
await async_setup_component(hass, "persistent_notification", {})
|
||||
entity_registry = mock_registry(hass)
|
||||
device_registry = mock_device_registry(hass)
|
||||
|
||||
vehicle_proxy = await create_vehicle_proxy(hass, vehicle_type)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.renault.RenaultHub.vehicles",
|
||||
new_callable=PropertyMock,
|
||||
return_value={
|
||||
vehicle_proxy.details.vin: vehicle_proxy,
|
||||
},
|
||||
), patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]):
|
||||
await setup_renault_integration(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_vehicle = MOCK_VEHICLES[vehicle_type]
|
||||
assert len(device_registry.devices) == 1
|
||||
expected_device = mock_vehicle["expected_device"]
|
||||
registry_entry = device_registry.async_get_device(expected_device["identifiers"])
|
||||
assert registry_entry is not None
|
||||
assert registry_entry.identifiers == expected_device["identifiers"]
|
||||
assert registry_entry.manufacturer == expected_device["manufacturer"]
|
||||
assert registry_entry.name == expected_device["name"]
|
||||
assert registry_entry.model == expected_device["model"]
|
||||
assert registry_entry.sw_version == expected_device["sw_version"]
|
||||
|
||||
expected_entities = mock_vehicle[SENSOR_DOMAIN]
|
||||
assert len(entity_registry.entities) == len(expected_entities)
|
||||
for expected_entity in expected_entities:
|
||||
entity_id = expected_entity["entity_id"]
|
||||
registry_entry = entity_registry.entities.get(entity_id)
|
||||
assert registry_entry is not None
|
||||
assert registry_entry.unique_id == expected_entity["unique_id"]
|
||||
assert registry_entry.unit_of_measurement == expected_entity.get("unit")
|
||||
assert registry_entry.device_class == expected_entity.get("class")
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == expected_entity["result"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys())
|
||||
async def test_sensor_empty(hass, vehicle_type):
|
||||
"""Test for Renault sensors with empty data from Renault."""
|
||||
await async_setup_component(hass, "persistent_notification", {})
|
||||
entity_registry = mock_registry(hass)
|
||||
device_registry = mock_device_registry(hass)
|
||||
|
||||
vehicle_proxy = await create_vehicle_proxy_with_side_effect(hass, vehicle_type, {})
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.renault.RenaultHub.vehicles",
|
||||
new_callable=PropertyMock,
|
||||
return_value={
|
||||
vehicle_proxy.details.vin: vehicle_proxy,
|
||||
},
|
||||
), patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]):
|
||||
await setup_renault_integration(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_vehicle = MOCK_VEHICLES[vehicle_type]
|
||||
assert len(device_registry.devices) == 1
|
||||
expected_device = mock_vehicle["expected_device"]
|
||||
registry_entry = device_registry.async_get_device(expected_device["identifiers"])
|
||||
assert registry_entry is not None
|
||||
assert registry_entry.identifiers == expected_device["identifiers"]
|
||||
assert registry_entry.manufacturer == expected_device["manufacturer"]
|
||||
assert registry_entry.name == expected_device["name"]
|
||||
assert registry_entry.model == expected_device["model"]
|
||||
assert registry_entry.sw_version == expected_device["sw_version"]
|
||||
|
||||
expected_entities = mock_vehicle[SENSOR_DOMAIN]
|
||||
assert len(entity_registry.entities) == len(expected_entities)
|
||||
for expected_entity in expected_entities:
|
||||
entity_id = expected_entity["entity_id"]
|
||||
registry_entry = entity_registry.entities.get(entity_id)
|
||||
assert registry_entry is not None
|
||||
assert registry_entry.unique_id == expected_entity["unique_id"]
|
||||
assert registry_entry.unit_of_measurement == expected_entity.get("unit")
|
||||
assert registry_entry.device_class == expected_entity.get("class")
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
@pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys())
|
||||
async def test_sensor_errors(hass, vehicle_type):
|
||||
"""Test for Renault sensors with temporary failure."""
|
||||
await async_setup_component(hass, "persistent_notification", {})
|
||||
entity_registry = mock_registry(hass)
|
||||
device_registry = mock_device_registry(hass)
|
||||
|
||||
invalid_upstream_exception = exceptions.InvalidUpstreamException(
|
||||
"err.tech.500",
|
||||
"Invalid response from the upstream server (The request sent to the GDC is erroneous) ; 502 Bad Gateway",
|
||||
)
|
||||
|
||||
vehicle_proxy = await create_vehicle_proxy_with_side_effect(
|
||||
hass, vehicle_type, invalid_upstream_exception
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.renault.RenaultHub.vehicles",
|
||||
new_callable=PropertyMock,
|
||||
return_value={
|
||||
vehicle_proxy.details.vin: vehicle_proxy,
|
||||
},
|
||||
), patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]):
|
||||
await setup_renault_integration(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_vehicle = MOCK_VEHICLES[vehicle_type]
|
||||
assert len(device_registry.devices) == 1
|
||||
expected_device = mock_vehicle["expected_device"]
|
||||
registry_entry = device_registry.async_get_device(expected_device["identifiers"])
|
||||
assert registry_entry is not None
|
||||
assert registry_entry.identifiers == expected_device["identifiers"]
|
||||
assert registry_entry.manufacturer == expected_device["manufacturer"]
|
||||
assert registry_entry.name == expected_device["name"]
|
||||
assert registry_entry.model == expected_device["model"]
|
||||
assert registry_entry.sw_version == expected_device["sw_version"]
|
||||
|
||||
expected_entities = mock_vehicle[SENSOR_DOMAIN]
|
||||
assert len(entity_registry.entities) == len(expected_entities)
|
||||
for expected_entity in expected_entities:
|
||||
entity_id = expected_entity["entity_id"]
|
||||
registry_entry = entity_registry.entities.get(entity_id)
|
||||
assert registry_entry is not None
|
||||
assert registry_entry.unique_id == expected_entity["unique_id"]
|
||||
assert registry_entry.unit_of_measurement == expected_entity.get("unit")
|
||||
assert registry_entry.device_class == expected_entity.get("class")
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_sensor_access_denied(hass):
|
||||
"""Test for Renault sensors with access denied failure."""
|
||||
await async_setup_component(hass, "persistent_notification", {})
|
||||
entity_registry = mock_registry(hass)
|
||||
device_registry = mock_device_registry(hass)
|
||||
|
||||
access_denied_exception = exceptions.AccessDeniedException(
|
||||
"err.func.403",
|
||||
"Access is denied for this resource",
|
||||
)
|
||||
|
||||
vehicle_proxy = await create_vehicle_proxy_with_side_effect(
|
||||
hass, "zoe_40", access_denied_exception
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.renault.RenaultHub.vehicles",
|
||||
new_callable=PropertyMock,
|
||||
return_value={
|
||||
vehicle_proxy.details.vin: vehicle_proxy,
|
||||
},
|
||||
), patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]):
|
||||
await setup_renault_integration(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(device_registry.devices) == 0
|
||||
assert len(entity_registry.entities) == 0
|
||||
|
||||
|
||||
async def test_sensor_not_supported(hass):
|
||||
"""Test for Renault sensors with access denied failure."""
|
||||
await async_setup_component(hass, "persistent_notification", {})
|
||||
entity_registry = mock_registry(hass)
|
||||
device_registry = mock_device_registry(hass)
|
||||
|
||||
not_supported_exception = exceptions.NotSupportedException(
|
||||
"err.tech.501",
|
||||
"This feature is not technically supported by this gateway",
|
||||
)
|
||||
|
||||
vehicle_proxy = await create_vehicle_proxy_with_side_effect(
|
||||
hass, "zoe_40", not_supported_exception
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.renault.RenaultHub.vehicles",
|
||||
new_callable=PropertyMock,
|
||||
return_value={
|
||||
vehicle_proxy.details.vin: vehicle_proxy,
|
||||
},
|
||||
), patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]):
|
||||
await setup_renault_integration(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(device_registry.devices) == 0
|
||||
assert len(entity_registry.entities) == 0
|
18
tests/fixtures/renault/battery_status_charging.json
vendored
Normal file
18
tests/fixtures/renault/battery_status_charging.json
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"data": {
|
||||
"type": "Car",
|
||||
"id": "VF1AAAAA555777999",
|
||||
"attributes": {
|
||||
"timestamp": "2020-01-12T21:40:16Z",
|
||||
"batteryLevel": 60,
|
||||
"batteryTemperature": 20,
|
||||
"batteryAutonomy": 141,
|
||||
"batteryCapacity": 0,
|
||||
"batteryAvailableEnergy": 31,
|
||||
"plugStatus": 1,
|
||||
"chargingStatus": 1.0,
|
||||
"chargingRemainingTime": 145,
|
||||
"chargingInstantaneousPower": 27
|
||||
}
|
||||
}
|
||||
}
|
15
tests/fixtures/renault/battery_status_not_charging.json
vendored
Normal file
15
tests/fixtures/renault/battery_status_not_charging.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"data": {
|
||||
"type": "Car",
|
||||
"id": "VF1AAAAA555777999",
|
||||
"attributes": {
|
||||
"timestamp": "2020-11-17T09:06:48+01:00",
|
||||
"batteryLevel": 50,
|
||||
"batteryAutonomy": 128,
|
||||
"batteryCapacity": 0,
|
||||
"batteryAvailableEnergy": 0,
|
||||
"plugStatus": 0,
|
||||
"chargingStatus": -1.0
|
||||
}
|
||||
}
|
||||
}
|
7
tests/fixtures/renault/charge_mode_always.json
vendored
Normal file
7
tests/fixtures/renault/charge_mode_always.json
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"data": {
|
||||
"type": "Car",
|
||||
"id": "VF1AAAAA555777999",
|
||||
"attributes": { "chargeMode": "always" }
|
||||
}
|
||||
}
|
7
tests/fixtures/renault/charge_mode_schedule.json
vendored
Normal file
7
tests/fixtures/renault/charge_mode_schedule.json
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"data": {
|
||||
"type": "Car",
|
||||
"id": "VF1AAAAA555777999",
|
||||
"attributes": { "chargeMode": "schedule_mode" }
|
||||
}
|
||||
}
|
9
tests/fixtures/renault/cockpit_ev.json
vendored
Normal file
9
tests/fixtures/renault/cockpit_ev.json
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"data": {
|
||||
"type": "Car",
|
||||
"id": "VF1AAAAA555777999",
|
||||
"attributes": {
|
||||
"totalMileage": 49114.27
|
||||
}
|
||||
}
|
||||
}
|
11
tests/fixtures/renault/cockpit_fuel.json
vendored
Normal file
11
tests/fixtures/renault/cockpit_fuel.json
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"data": {
|
||||
"type": "Car",
|
||||
"id": "VF1AAAAA555777123",
|
||||
"attributes": {
|
||||
"fuelAutonomy": 35.0,
|
||||
"fuelQuantity": 3.0,
|
||||
"totalMileage": 5566.78
|
||||
}
|
||||
}
|
||||
}
|
7
tests/fixtures/renault/hvac_status.json
vendored
Normal file
7
tests/fixtures/renault/hvac_status.json
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"data": {
|
||||
"type": "Car",
|
||||
"id": "VF1AAAAA555777999",
|
||||
"attributes": { "externalTemperature": 8.0, "hvacStatus": "off" }
|
||||
}
|
||||
}
|
108
tests/fixtures/renault/vehicle_captur_fuel.json
vendored
Normal file
108
tests/fixtures/renault/vehicle_captur_fuel.json
vendored
Normal file
@ -0,0 +1,108 @@
|
||||
{
|
||||
"accountId": "account-id-1",
|
||||
"country": "LU",
|
||||
"vehicleLinks": [
|
||||
{
|
||||
"brand": "RENAULT",
|
||||
"vin": "VF1AAAAA555777123",
|
||||
"status": "ACTIVE",
|
||||
"linkType": "USER",
|
||||
"garageBrand": "RENAULT",
|
||||
"mileage": 346,
|
||||
"startDate": "2020-06-12",
|
||||
"createdDate": "2020-06-12T15:02:00.555432Z",
|
||||
"lastModifiedDate": "2020-06-15T06:21:43.762467Z",
|
||||
"cancellationReason": {},
|
||||
"connectedDriver": {
|
||||
"role": "MAIN_DRIVER",
|
||||
"createdDate": "2020-06-15T06:20:39.107794Z",
|
||||
"lastModifiedDate": "2020-06-15T06:20:39.107794Z"
|
||||
},
|
||||
"vehicleDetails": {
|
||||
"vin": "VF1AAAAA555777123",
|
||||
"engineType": "H5H",
|
||||
"engineRatio": "470",
|
||||
"modelSCR": "CP1",
|
||||
"deliveryCountry": {
|
||||
"code": "BE",
|
||||
"label": "BELGIQUE"
|
||||
},
|
||||
"family": {
|
||||
"code": "XJB",
|
||||
"label": "FAMILLE B+X OVER",
|
||||
"group": "007"
|
||||
},
|
||||
"tcu": {
|
||||
"code": "AIVCT",
|
||||
"label": "AVEC BOITIER CONNECT AIVC",
|
||||
"group": "E70"
|
||||
},
|
||||
"navigationAssistanceLevel": {
|
||||
"code": "",
|
||||
"label": "",
|
||||
"group": ""
|
||||
},
|
||||
"battery": {
|
||||
"code": "SANBAT",
|
||||
"label": "SANS BATTERIE",
|
||||
"group": "968"
|
||||
},
|
||||
"radioType": {
|
||||
"code": "NA406",
|
||||
"label": "A-IVIMINDL, 2BO + 2BI + 2T, MICRO-DOUBLE, FM1/DAB+FM2",
|
||||
"group": "425"
|
||||
},
|
||||
"registrationCountry": {
|
||||
"code": "BE"
|
||||
},
|
||||
"brand": {
|
||||
"label": "RENAULT"
|
||||
},
|
||||
"model": {
|
||||
"code": "XJB1SU",
|
||||
"label": "CAPTUR II",
|
||||
"group": "971"
|
||||
},
|
||||
"gearbox": {
|
||||
"code": "BVA7",
|
||||
"label": "BOITE DE VITESSE AUTOMATIQUE 7 RAPPORTS",
|
||||
"group": "427"
|
||||
},
|
||||
"version": {
|
||||
"code": "ITAMFHA 6TH"
|
||||
},
|
||||
"energy": {
|
||||
"code": "ESS",
|
||||
"label": "ESSENCE",
|
||||
"group": "019"
|
||||
},
|
||||
"registrationNumber": "REG-NUMBER",
|
||||
"vcd": "ADR00/DLIGM2/PGPRT2/FEUAR3/CDVIT1/SKTPOU/SKTPGR/SSCCPC/SSPREM/FDIU2/MAPSTD/RCALL/MET04/DANGMO/ECOMOD/SSRCAR/AIVCT/AVGSI/TPRPE/TSGNE/2TON/ITPK7/MLEXP1/SPERTA/SSPERG/SPERTP/VOLCHA/SREACT/AVOSP1/SWALBO/DWGE01/AVC1A/1234Y/AEBS07/PRAHL/AVCAM/STANDA/XJB/HJB/EA3/MF/ESS/DG/TEMP/TR4X2/AFURGE/RVDIST/ABS/SBARTO/CA02/TOPAN/PBNCH/LAC/VSTLAR/CPE/RET04/2RVLG/RALU17/CEAVRH/AIRBA2/SERIE/DRA/DRAP05/HARM01/ATAR03/SGAV02/SGAR02/BIXPE/BANAL/KM/TPRM3/AVREPL/SSDECA/SFIRBA/ABLAVI/ESPHSA/FPAS2/ALEVA/SCACBA/SOP03C/SSADPC/STHPLG/SKTGRV/VLCUIR/RETIN2/TRSEV1/REPNTC/LVAVIP/LVAREI/SASURV/KTGREP/SGACHA/BEL01/APL03/FSTPO/ALOUC5/CMAR3P/FIPOU2/NA406/BVA7/ECLHB4/RDIF10/PNSTRD/ISOFIX/ENPH01/HRGM01/SANFLT/CSRGAC/SANACF/SDPCLV/TLRP00/SPRODI/SAN613/AVFAP/AIRBDE/CHC03/E06T/SAN806/SSPTLP/SANCML/SSFLEX/SDRQAR/SEXTIN/M2019/PHAS1/SPRTQT/SAN913/STHABT/SSTYAD/HYB01/SSCABA/SANBAT/VEC012/XJB1SU/SSNBT/H5H",
|
||||
"assets": [
|
||||
{
|
||||
"assetType": "PICTURE",
|
||||
"renditions": [
|
||||
{
|
||||
"resolutionType": "ONE_MYRENAULT_LARGE",
|
||||
"url": "https: //3dv2.renault.com/ImageFromBookmark?configuration=ADR00%2FDLIGM2%2FPGPRT2%2FFEUAR3%2FCDVIT1%2FSSCCPC%2FRCALL%2FMET04%2FDANGMO%2FSSRCAR%2FAVGSI%2FITPK7%2FMLEXP1%2FSPERTA%2FSSPERG%2FSPERTP%2FVOLCHA%2FSREACT%2FDWGE01%2FAVCAM%2FHJB%2FEA3%2FESS%2FDG%2FTEMP%2FTR4X2%2FRVDIST%2FSBARTO%2FCA02%2FTOPAN%2FPBNCH%2FVSTLAR%2FCPE%2FRET04%2FRALU17%2FDRA%2FDRAP05%2FHARM01%2FBIXPE%2FKM%2FSSDECA%2FESPHSA%2FFPAS2%2FALEVA%2FSOP03C%2FSSADPC%2FVLCUIR%2FRETIN2%2FREPNTC%2FLVAVIP%2FLVAREI%2FSGACHA%2FALOUC5%2FNA406%2FBVA7%2FECLHB4%2FRDIF10%2FCSRGAC%2FSANACF%2FTLRP00%2FAIRBDE%2FCHC03%2FSSPTLP%2FSPRTQT%2FSAN913%2FHYB01%2FH5H&databaseId=3e814da7-766d-4039-ac69-f001a1f738c8&bookmarkSet=RSITE&bookmark=EXT_34_DESSUS&profile=HELIOS_OWNERSERVICES_LARGE"
|
||||
},
|
||||
{
|
||||
"resolutionType": "ONE_MYRENAULT_SMALL",
|
||||
"url": "https: //3dv2.renault.com/ImageFromBookmark?configuration=ADR00%2FDLIGM2%2FPGPRT2%2FFEUAR3%2FCDVIT1%2FSSCCPC%2FRCALL%2FMET04%2FDANGMO%2FSSRCAR%2FAVGSI%2FITPK7%2FMLEXP1%2FSPERTA%2FSSPERG%2FSPERTP%2FVOLCHA%2FSREACT%2FDWGE01%2FAVCAM%2FHJB%2FEA3%2FESS%2FDG%2FTEMP%2FTR4X2%2FRVDIST%2FSBARTO%2FCA02%2FTOPAN%2FPBNCH%2FVSTLAR%2FCPE%2FRET04%2FRALU17%2FDRA%2FDRAP05%2FHARM01%2FBIXPE%2FKM%2FSSDECA%2FESPHSA%2FFPAS2%2FALEVA%2FSOP03C%2FSSADPC%2FVLCUIR%2FRETIN2%2FREPNTC%2FLVAVIP%2FLVAREI%2FSGACHA%2FALOUC5%2FNA406%2FBVA7%2FECLHB4%2FRDIF10%2FCSRGAC%2FSANACF%2FTLRP00%2FAIRBDE%2FCHC03%2FSSPTLP%2FSPRTQT%2FSAN913%2FHYB01%2FH5H&databaseId=3e814da7-766d-4039-ac69-f001a1f738c8&bookmarkSet=RSITE&bookmark=EXT_34_DESSUS&profile=HELIOS_OWNERSERVICES_SMALL_V2"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"yearsOfMaintenance": 12,
|
||||
"connectivityTechnology": "NONE",
|
||||
"easyConnectStore": false,
|
||||
"electrical": false,
|
||||
"rlinkStore": false,
|
||||
"deliveryDate": "2020-06-17",
|
||||
"retrievedFromDhs": false,
|
||||
"engineEnergyType": "OTHER",
|
||||
"radioCode": "1234"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
110
tests/fixtures/renault/vehicle_captur_phev.json
vendored
Normal file
110
tests/fixtures/renault/vehicle_captur_phev.json
vendored
Normal file
@ -0,0 +1,110 @@
|
||||
{
|
||||
"accountId": "account-id-2",
|
||||
"country": "IT",
|
||||
"vehicleLinks": [
|
||||
{
|
||||
"brand": "RENAULT",
|
||||
"vin": "VF1AAAAA555777123",
|
||||
"status": "ACTIVE",
|
||||
"linkType": "OWNER",
|
||||
"garageBrand": "RENAULT",
|
||||
"startDate": "2020-10-07",
|
||||
"createdDate": "2020-10-07T09:17:44.692802Z",
|
||||
"lastModifiedDate": "2021-03-28T10:44:01.139649Z",
|
||||
"ownershipStartDate": "2020-09-30",
|
||||
"cancellationReason": {},
|
||||
"connectedDriver": {
|
||||
"role": "MAIN_DRIVER",
|
||||
"createdDate": "2020-10-08T17:36:39.445523Z",
|
||||
"lastModifiedDate": "2020-10-08T17:36:39.445523Z"
|
||||
},
|
||||
"vehicleDetails": {
|
||||
"vin": "VF1AAAAA555777123",
|
||||
"registrationDate": "2020-09-30",
|
||||
"firstRegistrationDate": "2020-09-30",
|
||||
"engineType": "H4M",
|
||||
"engineRatio": "630",
|
||||
"modelSCR": "",
|
||||
"deliveryCountry": {
|
||||
"code": "IT",
|
||||
"label": "ITALY"
|
||||
},
|
||||
"family": {
|
||||
"code": "XJB",
|
||||
"label": "B+X OVER FAMILY",
|
||||
"group": "007"
|
||||
},
|
||||
"tcu": {
|
||||
"code": "AIVCT",
|
||||
"label": "WITH AIVC CONNECTION UNIT",
|
||||
"group": "E70"
|
||||
},
|
||||
"navigationAssistanceLevel": {
|
||||
"code": "",
|
||||
"label": "",
|
||||
"group": ""
|
||||
},
|
||||
"battery": {
|
||||
"code": "BT9AE1",
|
||||
"label": "BATTERY BT9AE1",
|
||||
"group": "968"
|
||||
},
|
||||
"radioType": {
|
||||
"code": "NA418",
|
||||
"label": "FULL NAV DAB ETH - AUDI",
|
||||
"group": "425"
|
||||
},
|
||||
"registrationCountry": {
|
||||
"code": "IT"
|
||||
},
|
||||
"brand": {
|
||||
"label": "RENAULT"
|
||||
},
|
||||
"model": {
|
||||
"code": "XJB1SU",
|
||||
"label": "CAPTUR II",
|
||||
"group": "971"
|
||||
},
|
||||
"gearbox": {
|
||||
"code": "BVH4",
|
||||
"label": "HYBRID 4 SPEED GEARBOX",
|
||||
"group": "427"
|
||||
},
|
||||
"version": {
|
||||
"code": "ITAMMHH 6UP"
|
||||
},
|
||||
"energy": {
|
||||
"code": "ESS",
|
||||
"label": "PETROL",
|
||||
"group": "019"
|
||||
},
|
||||
"registrationNumber": "REG-NUMBER",
|
||||
"vcd": "STANDA/XJB/HJB/EA3/MM/ESS/DG/TEMP/TR4X2/AFURGE/RV/ABS/SBARTO/CA02/TN/PBNCH/LAC/VT/CPE/RET04/2RVLG/RALU17/CEAVRH/AIRBA2/SERIE/DRA/DRAP05/HARM01/ATAR03/SGAV01/SGAR02/BIYPC/BANAL/KM/TPRM3/AVREPL/SSDECA/SFIRBA/ABLAVI/ESPHSA/FPAS2/ALEVA/CACBL3/SOP03C/SSADPC/STHPLG/SKTGRV/VLCUIR/RETRCR/TRSEV1/REPNTC/LVAVIP/LVAREI/SASURV/KTGREP/SGSCHA/ITA01/APL03/FSTPO/ALOUC5/PART01/CMAR3P/FIPOU2/NA418/BVH4/ECLHB4/RDIF10/PNSTRD/ISOFIX/ENPH01/HRGM01/SANFLT/CSRFLY/SANACF/SDPCLV/TLRP00/SPRODI/SAN613/AVFAP/AIRBDE/CHC03/E06U/SAN806/SSPTLP/SANCML/SSFLEX/SDRQAR/SEXTIN/M2019/PHAS1/SPRTQT/SAN913/STHABT/5DHS/HYB06/010KWH/BT9AE1/VEC237/XJB1SU/NBT018/H4M/NOADR/DLIGM2/PGPRT2/FEUAR3/SCDVIT/SKTPOU/SKTPGR/SSCCPC/SSPREM/FDIU2/MAPSTD/RCALL/MET05/SDANGM/ECOMOD/SSRCAR/AIVCT/AVGSI/TPQNW/TSGNE/2TON/ITPK4/MLEXP1/SPERTA/SSPERG/SPERTP/VOLNCH/SREACT/AVTSR1/SWALBO/DWGE01/AVC1A/VSPTA/1234Y/AEBS07/PRAHL/RRCAM",
|
||||
"assets": [
|
||||
{
|
||||
"assetType": "PICTURE",
|
||||
"renditions": [
|
||||
{
|
||||
"resolutionType": "ONE_MYRENAULT_LARGE",
|
||||
"url": "https://3dv2.renault.com/ImageFromBookmark?configuration=HJB%2FEA3%2FESS%2FDG%2FTEMP%2FTR4X2%2FRV%2FSBARTO%2FCA02%2FTN%2FPBNCH%2FVT%2FCPE%2FRET04%2FRALU17%2FDRA%2FDRAP05%2FHARM01%2FBIYPC%2FKM%2FSSDECA%2FESPHSA%2FFPAS2%2FALEVA%2FSOP03C%2FSSADPC%2FVLCUIR%2FRETRCR%2FREPNTC%2FLVAVIP%2FLVAREI%2FALOUC5%2FNA418%2FBVH4%2FECLHB4%2FRDIF10%2FCSRFLY%2FSANACF%2FTLRP00%2FAIRBDE%2FCHC03%2FSSPTLP%2FSPRTQT%2FSAN913%2FHYB06%2FH4M%2FNOADR%2FDLIGM2%2FPGPRT2%2FFEUAR3%2FSSCCPC%2FRCALL%2FMET05%2FSDANGM%2FSSRCAR%2FAVGSI%2FITPK4%2FMLEXP1%2FSPERTA%2FSSPERG%2FSPERTP%2FVOLNCH%2FSREACT%2FDWGE01%2FRRCAM&databaseId=b2b4fefb-d131-4f8f-9a24-4223c38bc710&bookmarkSet=CARPICKER&bookmark=EXT_34_RIGHT_FRONT&profile=HELIOS_OWNERSERVICES_LARGE"
|
||||
},
|
||||
{
|
||||
"resolutionType": "ONE_MYRENAULT_SMALL",
|
||||
"url": "https://3dv2.renault.com/ImageFromBookmark?configuration=HJB%2FEA3%2FESS%2FDG%2FTEMP%2FTR4X2%2FRV%2FSBARTO%2FCA02%2FTN%2FPBNCH%2FVT%2FCPE%2FRET04%2FRALU17%2FDRA%2FDRAP05%2FHARM01%2FBIYPC%2FKM%2FSSDECA%2FESPHSA%2FFPAS2%2FALEVA%2FSOP03C%2FSSADPC%2FVLCUIR%2FRETRCR%2FREPNTC%2FLVAVIP%2FLVAREI%2FALOUC5%2FNA418%2FBVH4%2FECLHB4%2FRDIF10%2FCSRFLY%2FSANACF%2FTLRP00%2FAIRBDE%2FCHC03%2FSSPTLP%2FSPRTQT%2FSAN913%2FHYB06%2FH4M%2FNOADR%2FDLIGM2%2FPGPRT2%2FFEUAR3%2FSSCCPC%2FRCALL%2FMET05%2FSDANGM%2FSSRCAR%2FAVGSI%2FITPK4%2FMLEXP1%2FSPERTA%2FSSPERG%2FSPERTP%2FVOLNCH%2FSREACT%2FDWGE01%2FRRCAM&databaseId=b2b4fefb-d131-4f8f-9a24-4223c38bc710&bookmarkSet=CARPICKER&bookmark=EXT_34_RIGHT_FRONT&profile=HELIOS_OWNERSERVICES_SMALL_V2"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"yearsOfMaintenance": 12,
|
||||
"connectivityTechnology": "NONE",
|
||||
"easyConnectStore": false,
|
||||
"electrical": false,
|
||||
"rlinkStore": false,
|
||||
"deliveryDate": "2020-09-30",
|
||||
"retrievedFromDhs": false,
|
||||
"engineEnergyType": "PHEV",
|
||||
"radioCode": "1234"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
189
tests/fixtures/renault/vehicle_zoe_40.json
vendored
Normal file
189
tests/fixtures/renault/vehicle_zoe_40.json
vendored
Normal file
@ -0,0 +1,189 @@
|
||||
{
|
||||
"accountId": "account-id-1",
|
||||
"country": "FR",
|
||||
"vehicleLinks": [
|
||||
{
|
||||
"brand": "RENAULT",
|
||||
"vin": "VF1AAAAA555777999",
|
||||
"status": "ACTIVE",
|
||||
"linkType": "OWNER",
|
||||
"garageBrand": "RENAULT",
|
||||
"annualMileage": 16000,
|
||||
"mileage": 26464,
|
||||
"startDate": "2017-08-07",
|
||||
"createdDate": "2019-05-23T21:38:16.409008Z",
|
||||
"lastModifiedDate": "2020-11-17T08:41:40.497400Z",
|
||||
"ownershipStartDate": "2017-08-01",
|
||||
"cancellationReason": {},
|
||||
"connectedDriver": {
|
||||
"role": "MAIN_DRIVER",
|
||||
"createdDate": "2019-06-17T09:49:06.880627Z",
|
||||
"lastModifiedDate": "2019-06-17T09:49:06.880627Z"
|
||||
},
|
||||
"vehicleDetails": {
|
||||
"vin": "VF1AAAAA555777999",
|
||||
"registrationDate": "2017-08-01",
|
||||
"firstRegistrationDate": "2017-08-01",
|
||||
"engineType": "5AQ",
|
||||
"engineRatio": "601",
|
||||
"modelSCR": "ZOE",
|
||||
"deliveryCountry": {
|
||||
"code": "FR",
|
||||
"label": "FRANCE"
|
||||
},
|
||||
"family": {
|
||||
"code": "X10",
|
||||
"label": "FAMILLE X10",
|
||||
"group": "007"
|
||||
},
|
||||
"tcu": {
|
||||
"code": "TCU0G2",
|
||||
"label": "TCU VER 0 GEN 2",
|
||||
"group": "E70"
|
||||
},
|
||||
"navigationAssistanceLevel": {
|
||||
"code": "NAV3G5",
|
||||
"label": "LEVEL 3 TYPE 5 NAVIGATION",
|
||||
"group": "408"
|
||||
},
|
||||
"battery": {
|
||||
"code": "BT4AR1",
|
||||
"label": "BATTERIE BT4AR1",
|
||||
"group": "968"
|
||||
},
|
||||
"radioType": {
|
||||
"code": "RAD37A",
|
||||
"label": "RADIO 37A",
|
||||
"group": "425"
|
||||
},
|
||||
"registrationCountry": {
|
||||
"code": "FR"
|
||||
},
|
||||
"brand": {
|
||||
"label": "RENAULT"
|
||||
},
|
||||
"model": {
|
||||
"code": "X101VE",
|
||||
"label": "ZOE",
|
||||
"group": "971"
|
||||
},
|
||||
"gearbox": {
|
||||
"code": "BVEL",
|
||||
"label": "BOITE A VARIATEUR ELECTRIQUE",
|
||||
"group": "427"
|
||||
},
|
||||
"version": {
|
||||
"code": "INT MB 10R"
|
||||
},
|
||||
"energy": {
|
||||
"code": "ELEC",
|
||||
"label": "ELECTRIQUE",
|
||||
"group": "019"
|
||||
},
|
||||
"registrationNumber": "REG-NUMBER",
|
||||
"vcd": "SYTINC/SKTPOU/SAND41/FDIU1/SSESM/MAPSUP/SSCALL/SAND88/SAND90/SQKDRO/SDIFPA/FACBA2/PRLEX1/SSRCAR/CABDO2/TCU0G2/SWALBO/EVTEC1/STANDA/X10/B10/EA2/MB/ELEC/DG/TEMP/TR4X2/RV/ABS/CAREG/LAC/VT003/CPE/RET03/SPROJA/RALU16/CEAVRH/AIRBA1/SERIE/DRA/DRAP08/HARM02/ATAR/TERQG/SFBANA/KM/DPRPN/AVREPL/SSDECA/ASRESP/RDAR02/ALEVA/CACBL2/SOP02C/CTHAB2/TRNOR/LVAVIP/LVAREL/SASURV/KTGREP/SGSCHA/APL03/ALOUCC/CMAR3P/NAV3G5/RAD37A/BVEL/AUTAUG/RNORM/ISOFIX/EQPEUR/HRGM01/SDPCLV/TLFRAN/SPRODI/SAN613/SSAPEX/GENEV1/ELC1/SANCML/PE2012/PHAS1/SAN913/045KWH/BT4AR1/VEC153/X101VE/NBT017/5AQ",
|
||||
"assets": [
|
||||
{
|
||||
"assetType": "PICTURE",
|
||||
"renditions": [
|
||||
{
|
||||
"resolutionType": "ONE_MYRENAULT_LARGE",
|
||||
"url": "https://3dv2.renault.com/ImageFromBookmark?configuration=SKTPOU%2FPRLEX1%2FSTANDA%2FB10%2FEA2%2FDG%2FVT003%2FRET03%2FRALU16%2FDRAP08%2FHARM02%2FTERQG%2FRDAR02%2FALEVA%2FSOP02C%2FTRNOR%2FLVAVIP%2FLVAREL%2FNAV3G5%2FRAD37A%2FSDPCLV%2FTLFRAN%2FGENEV1%2FSAN913%2FBT4AR1%2FNBT017&databaseId=1d514feb-93a6-4b45-8785-e11d2a6f1864&bookmarkSet=RSITE&bookmark=EXT_34_DESSUS&profile=HELIOS_OWNERSERVICES_LARGE"
|
||||
},
|
||||
{
|
||||
"resolutionType": "ONE_MYRENAULT_SMALL",
|
||||
"url": "https://3dv2.renault.com/ImageFromBookmark?configuration=SKTPOU%2FPRLEX1%2FSTANDA%2FB10%2FEA2%2FDG%2FVT003%2FRET03%2FRALU16%2FDRAP08%2FHARM02%2FTERQG%2FRDAR02%2FALEVA%2FSOP02C%2FTRNOR%2FLVAVIP%2FLVAREL%2FNAV3G5%2FRAD37A%2FSDPCLV%2FTLFRAN%2FGENEV1%2FSAN913%2FBT4AR1%2FNBT017&databaseId=1d514feb-93a6-4b45-8785-e11d2a6f1864&bookmarkSet=RSITE&bookmark=EXT_34_DESSUS&profile=HELIOS_OWNERSERVICES_SMALL_V2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"assetType": "PDF",
|
||||
"assetRole": "GUIDE",
|
||||
"title": "PDF Guide",
|
||||
"description": "",
|
||||
"renditions": [
|
||||
{
|
||||
"url": "https://cdn.group.renault.com/ren/gb/myr/assets/x101ve/manual.pdf.asset.pdf/1558704861676.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"assetType": "URL",
|
||||
"assetRole": "GUIDE",
|
||||
"title": "e-guide",
|
||||
"description": "",
|
||||
"renditions": [
|
||||
{
|
||||
"url": "http://gb.e-guide.renault.com/eng/Zoe"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"assetType": "VIDEO",
|
||||
"assetRole": "CAR",
|
||||
"title": "10 Fundamentals about getting the best out of your electric vehicle",
|
||||
"description": "",
|
||||
"renditions": [
|
||||
{
|
||||
"url": "39r6QEKcOM4"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"assetType": "VIDEO",
|
||||
"assetRole": "CAR",
|
||||
"title": "Automatic Climate Control",
|
||||
"description": "",
|
||||
"renditions": [
|
||||
{
|
||||
"url": "Va2FnZFo_GE"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"assetType": "URL",
|
||||
"assetRole": "CAR",
|
||||
"title": "More videos",
|
||||
"description": "",
|
||||
"renditions": [
|
||||
{
|
||||
"url": "https://www.youtube.com/watch?v=wfpCMkK1rKI"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"assetType": "VIDEO",
|
||||
"assetRole": "CAR",
|
||||
"title": "Charging the battery",
|
||||
"description": "",
|
||||
"renditions": [
|
||||
{
|
||||
"url": "RaEad8DjUJs"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"assetType": "VIDEO",
|
||||
"assetRole": "CAR",
|
||||
"title": "Charging the battery at a station with a flap",
|
||||
"description": "",
|
||||
"renditions": [
|
||||
{
|
||||
"url": "zJfd7fJWtr0"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"yearsOfMaintenance": 12,
|
||||
"connectivityTechnology": "RLINK1",
|
||||
"easyConnectStore": false,
|
||||
"electrical": true,
|
||||
"rlinkStore": false,
|
||||
"deliveryDate": "2017-08-11",
|
||||
"retrievedFromDhs": false,
|
||||
"engineEnergyType": "ELEC",
|
||||
"radioCode": "1234"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
161
tests/fixtures/renault/vehicle_zoe_50.json
vendored
Normal file
161
tests/fixtures/renault/vehicle_zoe_50.json
vendored
Normal file
@ -0,0 +1,161 @@
|
||||
{
|
||||
"country": "GB",
|
||||
"vehicleLinks": [
|
||||
{
|
||||
"preferredDealer": {
|
||||
"brand": "RENAULT",
|
||||
"createdDate": "2019-05-23T20:42:01.086661Z",
|
||||
"lastModifiedDate": "2019-05-23T20:42:01.086662Z",
|
||||
"dealerId": "dealer-id-1"
|
||||
},
|
||||
"garageBrand": "RENAULT",
|
||||
"vehicleDetails": {
|
||||
"assets": [
|
||||
{
|
||||
"assetType": "PICTURE",
|
||||
"renditions": [
|
||||
{
|
||||
"resolutionType": "ONE_MYRENAULT_LARGE",
|
||||
"url": "https://3dv2.renault.com/ImageFromBookmark?configuration=DLIGM2%2FKITPOU%2FDANGMO%2FITPK4%2FVOLNCH%2FREACTI%2FSSAEBS%2FPRAHL%2FRRCAM%2FX10%2FB10%2FEA3%2FDG%2FCAREG%2FVSTLAR%2FRET03%2FPROJAB%2FRALU16%2FDRAP13%2F3ATRPH%2FTELNJ%2FALEVA%2FVLCUIR%2FRETRCR%2FRETC%2FLVAREL%2FSGSCHA%2FNA418%2FRDIF01%2FTL01A%2FNBT022&databaseId=a864e752-b1b9-405e-9c3e-880073e36cc9&bookmarkSet=RSITE&bookmark=EXT_34_DESSUS&profile=HELIOS_OWNERSERVICES_LARGE"
|
||||
},
|
||||
{
|
||||
"resolutionType": "ONE_MYRENAULT_SMALL",
|
||||
"url": "https://3dv2.renault.com/ImageFromBookmark?configuration=DLIGM2%2FKITPOU%2FDANGMO%2FITPK4%2FVOLNCH%2FREACTI%2FSSAEBS%2FPRAHL%2FRRCAM%2FX10%2FB10%2FEA3%2FDG%2FCAREG%2FVSTLAR%2FRET03%2FPROJAB%2FRALU16%2FDRAP13%2F3ATRPH%2FTELNJ%2FALEVA%2FVLCUIR%2FRETRCR%2FRETC%2FLVAREL%2FSGSCHA%2FNA418%2FRDIF01%2FTL01A%2FNBT022&databaseId=a864e752-b1b9-405e-9c3e-880073e36cc9&bookmarkSet=RSITE&bookmark=EXT_34_DESSUS&profile=HELIOS_OWNERSERVICES_SMALL_V2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "PDF Guide",
|
||||
"description": "",
|
||||
"assetType": "PDF",
|
||||
"assetRole": "GUIDE",
|
||||
"renditions": [
|
||||
{
|
||||
"url": "https://cdn.group.renault.com/ren/gb/myr/assets/x102ve/manual.pdf.asset.pdf/1558696740707.pdf"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "e-guide",
|
||||
"description": "",
|
||||
"assetType": "URL",
|
||||
"assetRole": "GUIDE",
|
||||
"renditions": [
|
||||
{
|
||||
"url": "https://gb.e-guide.renault.com/eng/Zoe-ph2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "All-New ZOE: Welcome to your new car",
|
||||
"description": "",
|
||||
"assetType": "VIDEO",
|
||||
"assetRole": "CAR",
|
||||
"renditions": [
|
||||
{
|
||||
"url": "1OGwwmWHB6o"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Renault ZOE: All you need to know",
|
||||
"description": "",
|
||||
"assetType": "VIDEO",
|
||||
"assetRole": "CAR",
|
||||
"renditions": [
|
||||
{
|
||||
"url": "_BVH-Rd6e5I"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"engineType": "5AQ",
|
||||
"registrationCountry": {
|
||||
"code": "FR"
|
||||
},
|
||||
"radioType": {
|
||||
"group": "425",
|
||||
"code": "NA418",
|
||||
"label": " FULL NAV DAB ETH - AUDI"
|
||||
},
|
||||
"tcu": {
|
||||
"group": "E70",
|
||||
"code": "AIVCT",
|
||||
"label": "AVEC BOITIER CONNECT AIVC"
|
||||
},
|
||||
"brand": {
|
||||
"label": "RENAULT"
|
||||
},
|
||||
"deliveryDate": "2020-01-22",
|
||||
"engineEnergyType": "ELEC",
|
||||
"registrationDate": "2020-01-13",
|
||||
"gearbox": {
|
||||
"group": "427",
|
||||
"code": "BVEL",
|
||||
"label": "BOITE A VARIATEUR ELECTRIQUE"
|
||||
},
|
||||
"model": {
|
||||
"group": "971",
|
||||
"code": "X102VE",
|
||||
"label": "ZOE"
|
||||
},
|
||||
"electrical": true,
|
||||
"energy": {
|
||||
"group": "019",
|
||||
"code": "ELEC",
|
||||
"label": "ELECTRIQUE"
|
||||
},
|
||||
"navigationAssistanceLevel": {
|
||||
"group": "408",
|
||||
"code": "SAN408",
|
||||
"label": "CRITERE DE CONTEXTE"
|
||||
},
|
||||
"yearsOfMaintenance": 12,
|
||||
"rlinkStore": false,
|
||||
"radioCode": "1234",
|
||||
"registrationNumber": "REG-NUMBER",
|
||||
"modelSCR": "ZOE",
|
||||
"easyConnectStore": false,
|
||||
"engineRatio": "605",
|
||||
"battery": {
|
||||
"group": "968",
|
||||
"code": "BT4AR1",
|
||||
"label": "BATTERIE BT4AR1"
|
||||
},
|
||||
"vin": "VF1AAAAA555777999",
|
||||
"retrievedFromDhs": false,
|
||||
"vcd": "ASCOD0/DLIGM2/SSTINC/KITPOU/SKTPGR/SSCCPC/SDPSEC/FDIU2/SSMAP/SSCALL/FACBA1/DANGMO/SSRCAR/SSCABD/AIVCT/AVGSI/ITPK4/VOLNCH/REACTI/AVOSP1/SWALBO/SSDWGE/1234Y/SSAEBS/PRAHL/RRCAM/STANDA/X10/B10/EA3/MD/ELEC/DG/TEMP/TR4X2/AFURGE/RV/ABS/CAREG/LAC/VSTLAR/CPETIR/RET03/PROJAB/RALU16/CEAVRH/ADAC/AIRBA2/SERIE/DRA/DRAP13/HARM02/3ATRPH/SGAV01/BARRAB/TELNJ/SFBANA/KM/DPRPN/AVREPL/SSDECA/ABLAV/ASRESP/ALEVA/SCACBA/SOP02C/STHPLG/SKTGRV/VLCUIR/RETRCR/TRSEV1/RETC/LVAVIP/LVAREL/SASURV/KTGREP/SGSCHA/FRA01/APL03/FSTPO/ALOUC5/CMAR3P/SAN408/NA418/BVEL/AUTAUG/SPREST/RDIF01/ISOFIX/EQPEUR/HRGM01/SDPCLV/CHASTD/TL01A/SPRODI/SAN613/AIRBDE/PSMREC/ELC1/SSPTLP/SANCML/SEXTIN/PE2019/PHAS2/SAN913/THABT2/SSTYAD/SSHYB/052KWH/BT4AR1/VEC018/X102VE/NBT022/5AQ",
|
||||
"firstRegistrationDate": "2020-01-13",
|
||||
"deliveryCountry": {
|
||||
"code": "FR",
|
||||
"label": "FRANCE"
|
||||
},
|
||||
"connectivityTechnology": "RLINK1",
|
||||
"family": {
|
||||
"group": "007",
|
||||
"code": "X10",
|
||||
"label": "FAMILLE X10"
|
||||
},
|
||||
"version": {
|
||||
"code": "INT A MD 1L"
|
||||
}
|
||||
},
|
||||
"status": "ACTIVE",
|
||||
"createdDate": "2020-08-21T16:48:00.243967Z",
|
||||
"cancellationReason": {},
|
||||
"linkType": "OWNER",
|
||||
"connectedDriver": {
|
||||
"role": "MAIN_DRIVER",
|
||||
"lastModifiedDate": "2020-08-22T09:41:53.477398Z",
|
||||
"createdDate": "2020-08-22T09:41:53.477398Z"
|
||||
},
|
||||
"vin": "VF1AAAAA555777999",
|
||||
"lastModifiedDate": "2020-11-29T22:01:21.162572Z",
|
||||
"brand": "RENAULT",
|
||||
"startDate": "2020-08-21",
|
||||
"ownershipStartDate": "2020-01-13",
|
||||
"ownershipEndDate": "2020-08-21"
|
||||
}
|
||||
],
|
||||
"accountId": "account-id-1"
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user