Make renault scan interval dynamic (#142964)

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
This commit is contained in:
tmenguy 2025-04-19 00:51:41 -07:00 committed by GitHub
parent 27b7fb6f91
commit c422bcf1e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 416 additions and 6 deletions

View File

@ -7,7 +7,9 @@ DOMAIN = "renault"
CONF_LOCALE = "locale"
CONF_KAMEREON_ACCOUNT_ID = "kamereon_account_id"
DEFAULT_SCAN_INTERVAL = 420 # 7 minutes
# normal number of allowed calls per hour to the API
# for a single car and the 7 coordinator, it is a scan every 7mn
MAX_CALLS_PER_HOURS = 60
# If throttled time to pause the updates, in seconds
COOLING_UPDATES_SECONDS = 60 * 15 # 15 minutes

View File

@ -32,9 +32,9 @@ from time import time
from .const import (
CONF_KAMEREON_ACCOUNT_ID,
COOLING_UPDATES_SECONDS,
DEFAULT_SCAN_INTERVAL,
MAX_CALLS_PER_HOURS,
)
from .renault_vehicle import RenaultVehicleProxy
from .renault_vehicle import COORDINATORS, RenaultVehicleProxy
LOGGER = logging.getLogger(__name__)
@ -82,7 +82,6 @@ class RenaultHub:
async def async_initialise(self, config_entry: RenaultConfigEntry) -> 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()
@ -94,6 +93,12 @@ class RenaultHub:
raise ConfigEntryNotReady(
"Failed to retrieve vehicle details from Renault servers"
)
num_call_per_scan = len(COORDINATORS) * len(vehicles.vehicleLinks)
scan_interval = timedelta(
seconds=(3600 * num_call_per_scan) / MAX_CALLS_PER_HOURS
)
device_registry = dr.async_get(self._hass)
await asyncio.gather(
*(
@ -108,6 +113,21 @@ class RenaultHub:
)
)
# all vehicles have been initiated with the right number of active coordinators
num_call_per_scan = 0
for vehicle_link in vehicles.vehicleLinks:
vehicle = self._vehicles[str(vehicle_link.vin)]
num_call_per_scan += len(vehicle.coordinators)
new_scan_interval = timedelta(
seconds=(3600 * num_call_per_scan) / MAX_CALLS_PER_HOURS
)
if new_scan_interval != scan_interval:
# we need to change the vehicles with the right scan interval
for vehicle_link in vehicles.vehicleLinks:
vehicle = self._vehicles[str(vehicle_link.vin)]
vehicle.update_scan_interval(new_scan_interval)
async def async_initialise_vehicle(
self,
vehicle_link: KamereonVehiclesLink,

View File

@ -91,6 +91,13 @@ class RenaultVehicleProxy:
self._scan_interval = scan_interval
self._hub = hub
def update_scan_interval(self, scan_interval: timedelta) -> None:
"""Set the scan interval for the vehicle."""
if scan_interval != self._scan_interval:
self._scan_interval = scan_interval
for coordinator in self.coordinators.values():
coordinator.update_interval = scan_interval
@property
def details(self) -> models.KamereonVehicleDetails:
"""Return the specs of the vehicle."""

View File

@ -0,0 +1,291 @@
{
"accountId": "account-id-2",
"country": "IT",
"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"
}
},
{
"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"
}
}
]
}

View File

@ -2,11 +2,15 @@
from collections.abc import Generator
import datetime
from unittest.mock import patch
from unittest.mock import AsyncMock, patch
from freezegun.api import FrozenDateTimeFactory
import pytest
from renault_api.kamereon.exceptions import QuotaLimitException
from renault_api.kamereon.exceptions import (
AccessDeniedException,
NotSupportedException,
QuotaLimitException,
)
from syrupy.assertion import SnapshotAssertion
from homeassistant.config_entries import ConfigEntry
@ -241,3 +245,89 @@ async def test_sensor_throttling_after_init(
assert not hass.states.get(entity_id).attributes.get(ATTR_ASSUMED_STATE)
assert "Renault API throttled" not in caplog.text
assert "Renault hub currently throttled: scan skipped" not in caplog.text
# scan interval in seconds = (3600 * num_calls) / MAX_CALLS_PER_HOURS
# MAX_CALLS_PER_HOURS being a constant, for now 60 calls per hour
# num_calls = num_coordinator_car_0 + num_coordinator_car_1 + ... + num_coordinator_car_n
@pytest.mark.parametrize(
("vehicle_type", "vehicle_count", "scan_interval"),
[
("zoe_40", 1, 300), # 5 coordinators => 5 minutes interval
("captur_fuel", 1, 240), # 4 coordinators => 4 minutes interval
("multi", 2, 540), # 9 coordinators => 9 minutes interval
],
indirect=["vehicle_type"],
)
async def test_dynamic_scan_interval(
hass: HomeAssistant,
config_entry: ConfigEntry,
vehicle_count: int,
scan_interval: int,
freezer: FrozenDateTimeFactory,
fixtures_with_data: dict[str, AsyncMock],
) -> None:
"""Test scan interval."""
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert fixtures_with_data["cockpit"].call_count == vehicle_count
# 2 seconds before the expected scan interval > not called
freezer.tick(datetime.timedelta(seconds=scan_interval - 2))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert fixtures_with_data["cockpit"].call_count == vehicle_count
# 2 seconds after the expected scan interval > called
freezer.tick(datetime.timedelta(seconds=4))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert fixtures_with_data["cockpit"].call_count == vehicle_count * 2
# scan interval in seconds = (3600 * num_calls) / MAX_CALLS_PER_HOURS
# MAX_CALLS_PER_HOURS being a constant, for now 60 calls per hour
# num_calls = num_coordinator_car_0 + num_coordinator_car_1 + ... + num_coordinator_car_n
@pytest.mark.parametrize(
("vehicle_type", "vehicle_count", "scan_interval"),
[
("zoe_40", 1, 240), # (5-1) coordinators => 4 minutes interval
("captur_fuel", 1, 180), # (4-1) coordinators => 3 minutes interval
("multi", 2, 420), # (9-2) coordinators => 7 minutes interval
],
indirect=["vehicle_type"],
)
async def test_dynamic_scan_interval_failed_coordinator(
hass: HomeAssistant,
config_entry: ConfigEntry,
vehicle_count: int,
scan_interval: int,
freezer: FrozenDateTimeFactory,
fixtures_with_data: dict[str, AsyncMock],
) -> None:
"""Test scan interval."""
fixtures_with_data["battery_status"].side_effect = NotSupportedException(
"err.tech.501",
"This feature is not technically supported by this gateway",
)
fixtures_with_data["lock_status"].side_effect = AccessDeniedException(
"err.func.403",
"Access is denied for this resource",
)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert fixtures_with_data["cockpit"].call_count == vehicle_count
# 2 seconds before the expected scan interval > not called
freezer.tick(datetime.timedelta(seconds=scan_interval - 2))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert fixtures_with_data["cockpit"].call_count == vehicle_count
# 2 seconds after the expected scan interval > called
freezer.tick(datetime.timedelta(seconds=4))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert fixtures_with_data["cockpit"].call_count == vehicle_count * 2