mirror of
https://github.com/home-assistant/core.git
synced 2025-07-25 22:27:07 +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.repack
|
||||||
homeassistant.components.recorder.statistics
|
homeassistant.components.recorder.statistics
|
||||||
homeassistant.components.remote.*
|
homeassistant.components.remote.*
|
||||||
|
homeassistant.components.renault.*
|
||||||
homeassistant.components.rituals_perfume_genie.*
|
homeassistant.components.rituals_perfume_genie.*
|
||||||
homeassistant.components.scene.*
|
homeassistant.components.scene.*
|
||||||
homeassistant.components.select.*
|
homeassistant.components.select.*
|
||||||
|
@ -411,6 +411,7 @@ homeassistant/components/rainmachine/* @bachya
|
|||||||
homeassistant/components/random/* @fabaff
|
homeassistant/components/random/* @fabaff
|
||||||
homeassistant/components/recollect_waste/* @bachya
|
homeassistant/components/recollect_waste/* @bachya
|
||||||
homeassistant/components/rejseplanen/* @DarkFox
|
homeassistant/components/rejseplanen/* @DarkFox
|
||||||
|
homeassistant/components/renault/* @epenet
|
||||||
homeassistant/components/repetier/* @MTrab
|
homeassistant/components/repetier/* @MTrab
|
||||||
homeassistant/components/rflink/* @javicalle
|
homeassistant/components/rflink/* @javicalle
|
||||||
homeassistant/components/rfxtrx/* @danielhiversen @elupus @RobBie1221
|
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",
|
"rachio",
|
||||||
"rainmachine",
|
"rainmachine",
|
||||||
"recollect_waste",
|
"recollect_waste",
|
||||||
|
"renault",
|
||||||
"rfxtrx",
|
"rfxtrx",
|
||||||
"ring",
|
"ring",
|
||||||
"risco",
|
"risco",
|
||||||
|
11
mypy.ini
11
mypy.ini
@ -902,6 +902,17 @@ no_implicit_optional = true
|
|||||||
warn_return_any = true
|
warn_return_any = true
|
||||||
warn_unreachable = 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.*]
|
[mypy-homeassistant.components.rituals_perfume_genie.*]
|
||||||
check_untyped_defs = true
|
check_untyped_defs = true
|
||||||
disallow_incomplete_defs = true
|
disallow_incomplete_defs = true
|
||||||
|
@ -2015,6 +2015,9 @@ raspyrfm-client==1.2.8
|
|||||||
# homeassistant.components.rainmachine
|
# homeassistant.components.rainmachine
|
||||||
regenmaschine==3.1.5
|
regenmaschine==3.1.5
|
||||||
|
|
||||||
|
# homeassistant.components.renault
|
||||||
|
renault-api==0.1.4
|
||||||
|
|
||||||
# homeassistant.components.python_script
|
# homeassistant.components.python_script
|
||||||
restrictedpython==5.1
|
restrictedpython==5.1
|
||||||
|
|
||||||
|
@ -1109,6 +1109,9 @@ rachiopy==1.0.3
|
|||||||
# homeassistant.components.rainmachine
|
# homeassistant.components.rainmachine
|
||||||
regenmaschine==3.1.5
|
regenmaschine==3.1.5
|
||||||
|
|
||||||
|
# homeassistant.components.renault
|
||||||
|
renault-api==0.1.4
|
||||||
|
|
||||||
# homeassistant.components.python_script
|
# homeassistant.components.python_script
|
||||||
restrictedpython==5.1
|
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