Renault code quality improvements (#53680)

pull/54700/head
epenet 2021-08-16 13:49:04 +02:00 committed by GitHub
parent 2e56f66518
commit a204d7f807
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 159 additions and 71 deletions

View File

@ -26,7 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
hass.data.setdefault(DOMAIN, {})
await renault_hub.async_initialise(config_entry)
hass.data[DOMAIN][config_entry.unique_id] = renault_hub
hass.data[DOMAIN][config_entry.entry_id] = renault_hub
hass.config_entries.async_setup_platforms(config_entry, PLATFORMS)
@ -40,6 +40,6 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
)
if unload_ok:
hass.data[DOMAIN].pop(config_entry.unique_id)
hass.data[DOMAIN].pop(config_entry.entry_id)
return unload_ok

View File

@ -1,10 +1,12 @@
"""Proxy to handle account communication with Renault servers."""
from __future__ import annotations
import asyncio
from datetime import timedelta
import logging
from renault_api.gigya.exceptions import InvalidCredentialsException
from renault_api.kamereon.models import KamereonVehiclesLink
from renault_api.renault_account import RenaultAccount
from renault_api.renault_client import RenaultClient
@ -23,7 +25,6 @@ class RenaultHub:
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
@ -49,17 +50,33 @@ class RenaultHub:
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 asyncio.gather(
*(
self.async_initialise_vehicle(
vehicle_link, self._account, scan_interval
)
await vehicle.async_initialise()
self._vehicles[vehicle_link.vin] = vehicle
for vehicle_link in vehicles.vehicleLinks
)
)
async def async_initialise_vehicle(
self,
vehicle_link: KamereonVehiclesLink,
renault_account: RenaultAccount,
scan_interval: timedelta,
) -> None:
"""Set up proxy."""
assert vehicle_link.vin is not None
assert vehicle_link.vehicleDetails is not None
# Generate vehicle proxy
vehicle = RenaultVehicleProxy(
hass=self._hass,
vehicle=await renault_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."""

View File

@ -115,7 +115,7 @@ class RenaultVehicleProxy:
coordinator = self.coordinators[key]
if coordinator.not_supported:
# Remove endpoint as it is not supported for this vehicle.
LOGGER.error(
LOGGER.warning(
"Ignoring endpoint %s as it is not supported for this vehicle: %s",
coordinator.name,
coordinator.last_exception,
@ -123,7 +123,7 @@ class RenaultVehicleProxy:
del self.coordinators[key]
elif coordinator.access_denied:
# Remove endpoint as it is denied for this vehicle.
LOGGER.error(
LOGGER.warning(
"Ignoring endpoint %s as it is denied for this vehicle: %s",
coordinator.name,
coordinator.last_exception,

View File

@ -1,14 +1,14 @@
"""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_POWER,
DEVICE_CLASS_TEMPERATURE,
ENERGY_KILO_WATT_HOUR,
LENGTH_KILOMETERS,
PERCENTAGE,
POWER_KILO_WATT,
@ -18,8 +18,6 @@ from homeassistant.const import (
)
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,
@ -46,20 +44,20 @@ async def async_setup_entry(
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)
proxy: RenaultHub = hass.data[DOMAIN][config_entry.entry_id]
entities = get_entities(proxy)
async_add_entities(entities)
async def get_entities(proxy: RenaultHub) -> list[RenaultDataEntity]:
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))
entities.extend(get_vehicle_entities(vehicle))
return entities
async def get_vehicle_entities(vehicle: RenaultVehicleProxy) -> list[RenaultDataEntity]:
def get_vehicle_entities(vehicle: RenaultVehicleProxy) -> list[RenaultDataEntity]:
"""Create Renault entities for single vehicle."""
entities: list[RenaultDataEntity] = []
if "cockpit" in vehicle.coordinators:
@ -78,6 +76,9 @@ async def get_vehicle_entities(vehicle: RenaultVehicleProxy) -> list[RenaultData
entities.append(RenaultChargingPowerSensor(vehicle, "Charging Power"))
entities.append(RenaultPlugStateSensor(vehicle, "Plug State"))
entities.append(RenaultBatteryAutonomySensor(vehicle, "Battery Autonomy"))
entities.append(
RenaultBatteryAvailableEnergySensor(vehicle, "Battery Available Energy")
)
entities.append(RenaultBatteryTemperatureSensor(vehicle, "Battery Temperature"))
if "charge_mode" in vehicle.coordinators:
entities.append(RenaultChargeModeSensor(vehicle, "Charge Mode"))
@ -96,6 +97,18 @@ class RenaultBatteryAutonomySensor(RenaultBatteryDataEntity, SensorEntity):
return self.data.batteryAutonomy if self.data else None
class RenaultBatteryAvailableEnergySensor(RenaultBatteryDataEntity, SensorEntity):
"""Battery available energy sensor."""
_attr_device_class = DEVICE_CLASS_ENERGY
_attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR
@property
def native_value(self) -> float | None:
"""Return the state of this entity."""
return self.data.batteryAvailableEnergy if self.data else None
class RenaultBatteryLevelSensor(RenaultBatteryDataEntity, SensorEntity):
"""Battery Level sensor."""
@ -107,22 +120,6 @@ class RenaultBatteryLevelSensor(RenaultBatteryDataEntity, SensorEntity):
"""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."""
@ -163,7 +160,7 @@ class RenaultChargeStateSensor(RenaultBatteryDataEntity, SensorEntity):
def native_value(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
return charging_status.name.lower() if charging_status is not None else None
@property
def icon(self) -> str:
@ -186,7 +183,7 @@ class RenaultChargingRemainingTimeSensor(RenaultBatteryDataEntity, SensorEntity)
class RenaultChargingPowerSensor(RenaultBatteryDataEntity, SensorEntity):
"""Charging Power sensor."""
_attr_device_class = DEVICE_CLASS_ENERGY
_attr_device_class = DEVICE_CLASS_POWER
_attr_native_unit_of_measurement = POWER_KILO_WATT
@property
@ -209,11 +206,9 @@ class RenaultFuelAutonomySensor(RenaultCockpitDataEntity, SensorEntity):
@property
def native_value(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
)
if not self.data or self.data.fuelAutonomy is None:
return None
return round(self.data.fuelAutonomy)
class RenaultFuelQuantitySensor(RenaultCockpitDataEntity, SensorEntity):
@ -225,11 +220,9 @@ class RenaultFuelQuantitySensor(RenaultCockpitDataEntity, SensorEntity):
@property
def native_value(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
)
if not self.data or self.data.fuelQuantity is None:
return None
return round(self.data.fuelQuantity)
class RenaultMileageSensor(RenaultCockpitDataEntity, SensorEntity):
@ -241,11 +234,9 @@ class RenaultMileageSensor(RenaultCockpitDataEntity, SensorEntity):
@property
def native_value(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
)
if not self.data or self.data.totalMileage is None:
return None
return round(self.data.totalMileage)
class RenaultOutsideTemperatureSensor(RenaultHVACDataEntity, SensorEntity):
@ -269,7 +260,7 @@ class RenaultPlugStateSensor(RenaultBatteryDataEntity, SensorEntity):
def native_value(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
return plug_status.name.lower() if plug_status is not None else None
@property
def icon(self) -> str:

View File

@ -31,27 +31,27 @@ def get_mock_config_entry():
def get_fixtures(vehicle_type: str) -> dict[str, Any]:
"""Create a vehicle proxy for testing."""
mock_vehicle = MOCK_VEHICLES[vehicle_type]
mock_vehicle = MOCK_VEHICLES.get(vehicle_type, {"endpoints": {}})
return {
"battery_status": schemas.KamereonVehicleDataResponseSchema.loads(
load_fixture(f"renault/{mock_vehicle['endpoints']['battery_status']}")
if "battery_status" in mock_vehicle["endpoints"]
else "{}"
else load_fixture("renault/no_data.json")
).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 "{}"
else load_fixture("renault/no_data.json")
).get_attributes(schemas.KamereonVehicleChargeModeDataSchema),
"cockpit": schemas.KamereonVehicleDataResponseSchema.loads(
load_fixture(f"renault/{mock_vehicle['endpoints']['cockpit']}")
if "cockpit" in mock_vehicle["endpoints"]
else "{}"
else load_fixture("renault/no_data.json")
).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 "{}"
else load_fixture("renault/no_data.json")
).get_attributes(schemas.KamereonVehicleHvacStatusDataSchema),
}
@ -123,6 +123,55 @@ async def setup_renault_integration_vehicle(hass: HomeAssistant, vehicle_type: s
return config_entry
async def setup_renault_integration_vehicle_with_no_data(
hass: HomeAssistant, vehicle_type: str
):
"""Create the Renault integration."""
config_entry = get_mock_config_entry()
config_entry.add_to_hass(hass)
renault_account = RenaultAccount(
config_entry.unique_id,
websession=aiohttp_client.async_get_clientsession(hass),
)
mock_vehicle = MOCK_VEHICLES[vehicle_type]
mock_fixtures = get_fixtures("")
with patch("renault_api.renault_session.RenaultSession.login"), patch(
"renault_api.renault_client.RenaultClient.get_api_account",
return_value=renault_account,
), patch(
"renault_api.renault_account.RenaultAccount.get_vehicles",
return_value=(
schemas.KamereonVehiclesResponseSchema.loads(
load_fixture(f"renault/vehicle_{vehicle_type}.json")
)
),
), patch(
"renault_api.renault_vehicle.RenaultVehicle.supports_endpoint",
side_effect=mock_vehicle["endpoints_available"],
), patch(
"renault_api.renault_vehicle.RenaultVehicle.has_contract_for_endpoint",
return_value=True,
), patch(
"renault_api.renault_vehicle.RenaultVehicle.get_battery_status",
return_value=mock_fixtures["battery_status"],
), patch(
"renault_api.renault_vehicle.RenaultVehicle.get_charge_mode",
return_value=mock_fixtures["charge_mode"],
), patch(
"renault_api.renault_vehicle.RenaultVehicle.get_cockpit",
return_value=mock_fixtures["cockpit"],
), patch(
"renault_api.renault_vehicle.RenaultVehicle.get_hvac_status",
return_value=mock_fixtures["hvac_status"],
):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
return config_entry
async def setup_renault_integration_vehicle_with_side_effect(
hass: HomeAssistant, vehicle_type: str, side_effect: Any
):

View File

@ -13,7 +13,9 @@ from homeassistant.const import (
CONF_USERNAME,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_TEMPERATURE,
ENERGY_KILO_WATT_HOUR,
LENGTH_KILOMETERS,
PERCENTAGE,
POWER_KILO_WATT,
@ -59,6 +61,13 @@ MOCK_VEHICLES = {
"result": "141",
"unit": LENGTH_KILOMETERS,
},
{
"entity_id": "sensor.battery_available_energy",
"unique_id": "vf1aaaaa555777999_battery_available_energy",
"result": "31",
"unit": ENERGY_KILO_WATT_HOUR,
"class": DEVICE_CLASS_ENERGY,
},
{
"entity_id": "sensor.battery_level",
"unique_id": "vf1aaaaa555777999_battery_level",
@ -90,7 +99,7 @@ MOCK_VEHICLES = {
"unique_id": "vf1aaaaa555777999_charging_power",
"result": "0.027",
"unit": POWER_KILO_WATT,
"class": DEVICE_CLASS_ENERGY,
"class": DEVICE_CLASS_POWER,
},
{
"entity_id": "sensor.charging_remaining_time",
@ -145,6 +154,13 @@ MOCK_VEHICLES = {
"result": "128",
"unit": LENGTH_KILOMETERS,
},
{
"entity_id": "sensor.battery_available_energy",
"unique_id": "vf1aaaaa555777999_battery_available_energy",
"result": "0",
"unit": ENERGY_KILO_WATT_HOUR,
"class": DEVICE_CLASS_ENERGY,
},
{
"entity_id": "sensor.battery_level",
"unique_id": "vf1aaaaa555777999_battery_level",
@ -176,7 +192,7 @@ MOCK_VEHICLES = {
"unique_id": "vf1aaaaa555777999_charging_power",
"result": STATE_UNKNOWN,
"unit": POWER_KILO_WATT,
"class": DEVICE_CLASS_ENERGY,
"class": DEVICE_CLASS_POWER,
},
{
"entity_id": "sensor.charging_remaining_time",
@ -224,6 +240,13 @@ MOCK_VEHICLES = {
"result": "141",
"unit": LENGTH_KILOMETERS,
},
{
"entity_id": "sensor.battery_available_energy",
"unique_id": "vf1aaaaa555777123_battery_available_energy",
"result": "31",
"unit": ENERGY_KILO_WATT_HOUR,
"class": DEVICE_CLASS_ENERGY,
},
{
"entity_id": "sensor.battery_level",
"unique_id": "vf1aaaaa555777123_battery_level",
@ -255,7 +278,7 @@ MOCK_VEHICLES = {
"unique_id": "vf1aaaaa555777123_charging_power",
"result": "27.0",
"unit": POWER_KILO_WATT,
"class": DEVICE_CLASS_ENERGY,
"class": DEVICE_CLASS_POWER,
},
{
"entity_id": "sensor.charging_remaining_time",

View File

@ -17,13 +17,13 @@ async def test_setup_unload_entry(hass):
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert config_entry.state is ConfigEntryState.LOADED
assert config_entry.unique_id in hass.data[DOMAIN]
assert config_entry.entry_id in hass.data[DOMAIN]
# Unload the entry and verify that the data has been removed
await hass.config_entries.async_unload(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.NOT_LOADED
assert config_entry.unique_id not in hass.data[DOMAIN]
assert config_entry.entry_id not in hass.data[DOMAIN]
async def test_setup_entry_bad_password(hass):

View File

@ -5,11 +5,12 @@ 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.const import STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.setup import async_setup_component
from . import (
setup_renault_integration_vehicle,
setup_renault_integration_vehicle_with_no_data,
setup_renault_integration_vehicle_with_side_effect,
)
from .const import MOCK_VEHICLES
@ -60,7 +61,7 @@ async def test_sensor_empty(hass, vehicle_type):
device_registry = mock_device_registry(hass)
with patch("homeassistant.components.renault.PLATFORMS", [SENSOR_DOMAIN]):
await setup_renault_integration_vehicle_with_side_effect(hass, vehicle_type, {})
await setup_renault_integration_vehicle_with_no_data(hass, vehicle_type)
await hass.async_block_till_done()
mock_vehicle = MOCK_VEHICLES[vehicle_type]
@ -84,7 +85,7 @@ async def test_sensor_empty(hass, vehicle_type):
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
assert state.state == STATE_UNKNOWN
@pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys())

7
tests/fixtures/renault/no_data.json vendored Normal file
View File

@ -0,0 +1,7 @@
{
"data": {
"type": "Car",
"id": "VF1AAAAA555777999",
"attributes": {}
}
}