Skip to content

Commit

Permalink
feat: split coordinator to avoid updating disabled cars and energy si…
Browse files Browse the repository at this point in the history
…tes (#552)
  • Loading branch information
bdraco authored Mar 26, 2023
1 parent 7386d43 commit 41dfbcc
Show file tree
Hide file tree
Showing 16 changed files with 151 additions and 76 deletions.
75 changes: 62 additions & 13 deletions custom_components/tesla_custom/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Support for Tesla cars."""
import asyncio
from datetime import timedelta
from functools import partial
from http import HTTPStatus
import logging

Expand Down Expand Up @@ -215,9 +216,6 @@ def _async_create_close_task():
config_entry.async_on_unload(_async_create_close_task)

_async_save_tokens(hass, config_entry, access_token, refresh_token, expiration)
coordinator = TeslaDataUpdateCoordinator(
hass, config_entry=config_entry, controller=controller
)

try:
if config_entry.data.get("initial_setup"):
Expand Down Expand Up @@ -268,15 +266,38 @@ def _async_create_close_task():

return False

reload_lock = asyncio.Lock()
_partial_coordinator = partial(
TeslaDataUpdateCoordinator,
hass,
config_entry=config_entry,
controller=controller,
reload_lock=reload_lock,
energy_site_ids=set(),
vins=set(),
update_vehicles=False,
)
coordinators = {
"update_vehicles": _partial_coordinator(update_vehicles=True),
**{
energy_site_id: _partial_coordinator(energy_site_ids={energy_site_id})
for energy_site_id in energysites
},
**{vin: _partial_coordinator(vins={vin}) for vin in cars},

This comment has been minimized.

Copy link
@alandtse

alandtse Jun 1, 2023

Owner

@bdraco do you think the lack of update_vehicles=true for the vins line here is the cause of #621 and other issues people have been reporting about vehicles not getting updated? I'm not fully understanding what the **{vin} syntax is actually doing. I think the fix listed in 621 is probably wrong, but this line looks like the potential issue.

This comment has been minimized.

Copy link
@bdraco

bdraco Jun 1, 2023

Author Contributor

I think the problem is there are no subscribers to the "update_vehicles" coordinator so its deciding not to update

This comment has been minimized.

Copy link
@bdraco

bdraco Jun 1, 2023

Author Contributor
}

hass.data[DOMAIN][config_entry.entry_id] = {
"coordinator": coordinator,
"controller": controller,
"coordinators": coordinators,
"cars": cars,
"energysites": energysites,
DATA_LISTENER: [config_entry.add_update_listener(update_listener)],
}
_LOGGER.debug("Connected to the Tesla API")

await coordinator.async_config_entry_first_refresh()
# We do not do a first refresh as we already know the API is working
# from above. Each platform will schedule a refresh via update_before_add
# for the sites/vehicles they are interested in.

await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)

Expand All @@ -288,11 +309,11 @@ async def async_unload_entry(hass, config_entry) -> bool:
unload_ok = await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS
)
await hass.data[DOMAIN].get(config_entry.entry_id)[
"coordinator"
].controller.disconnect()
entry_data = hass.data[DOMAIN][config_entry.entry_id]
controller: TeslaAPI = entry_data["controller"]
await controller.disconnect()

for listener in hass.data[DOMAIN][config_entry.entry_id][DATA_LISTENER]:
for listener in entry_data[DATA_LISTENER]:
listener()
username = config_entry.title

Expand All @@ -310,7 +331,8 @@ async def async_unload_entry(hass, config_entry) -> bool:

async def update_listener(hass, config_entry):
"""Update when config_entry options update."""
controller = hass.data[DOMAIN][config_entry.entry_id]["coordinator"].controller
entry_data = hass.data[DOMAIN][config_entry.entry_id]
controller: TeslaAPI = entry_data["controller"]
old_update_interval = controller.update_interval
controller.update_interval = config_entry.options.get(
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
Expand All @@ -326,10 +348,24 @@ async def update_listener(hass, config_entry):
class TeslaDataUpdateCoordinator(DataUpdateCoordinator):
"""Class to manage fetching Tesla data."""

def __init__(self, hass, *, config_entry, controller: TeslaAPI):
def __init__(
self,
hass,
*,
config_entry,
controller: TeslaAPI,
reload_lock: asyncio.Lock,
vins: set[str],
energy_site_ids: set[str],
update_vehicles: bool,
):
"""Initialize global Tesla data updater."""
self.controller = controller
self.config_entry = config_entry
self.reload_lock = reload_lock
self.vins = vins
self.energy_site_ids = energy_site_ids
self.update_vehicles = update_vehicles

update_interval = timedelta(seconds=MIN_SCAN_INTERVAL)

Expand All @@ -343,6 +379,8 @@ def __init__(self, hass, *, config_entry, controller: TeslaAPI):
async def _async_update_data(self):
"""Fetch data from API endpoint."""
if self.controller.is_token_refreshed():
# It doesn't matter which coordinator calls this, as long as there
# are no awaits in the below code, it will be called only once.
result = self.controller.get_tokens()
refresh_token = result["refresh_token"]
access_token = result["access_token"]
Expand All @@ -357,8 +395,19 @@ async def _async_update_data(self):
# handled by the data update coordinator.
async with async_timeout.timeout(30):
_LOGGER.debug("Running controller.update()")
return await self.controller.update()
return await self.controller.update(
vins=self.vins,
energy_site_ids=self.energy_site_ids,
update_vehicles=self.update_vehicles,
)
except IncompleteCredentials:
await self.hass.config_entries.async_reload(self.config_entry.entry_id)
if self.reload_lock.locked():
# Any of the coordinators can trigger a reload, but we only
# want to do it once. If the lock is already locked, we know
# another coordinator is already reloading.
_LOGGER.debug("Config entry is already being reloaded")
return
async with self.reload_lock:
await self.hass.config_entries.async_reload(self.config_entry.entry_id)
except TeslaException as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err
15 changes: 9 additions & 6 deletions custom_components/tesla_custom/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@

async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entities):
"""Set up the Tesla selects by config_entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"]
cars = hass.data[DOMAIN][config_entry.entry_id]["cars"]
energysites = hass.data[DOMAIN][config_entry.entry_id]["energysites"]
entry_data = hass.data[DOMAIN][config_entry.entry_id]
coordinators = entry_data["coordinators"]
cars = entry_data["cars"]
energysites = entry_data["energysites"]
entities = []

for car in cars.values():
for vin, car in cars.items():
coordinator = coordinators[vin]
entities.append(TeslaCarParkingBrake(hass, car, coordinator))
entities.append(TeslaCarOnline(hass, car, coordinator))
entities.append(TeslaCarAsleep(hass, car, coordinator))
Expand All @@ -35,12 +37,13 @@ async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entitie
entities.append(TeslaCarScheduledDeparture(hass, car, coordinator))
entities.append(TeslaCarUserPresent(hass, car, coordinator))

for energysite in energysites.values():
for energy_site_id, energysite in energysites.items():
coordinator = coordinators[energy_site_id]
if energysite.resource_type == RESOURCE_TYPE_BATTERY:
entities.append(TeslaEnergyBatteryCharging(hass, energysite, coordinator))
entities.append(TeslaEnergyGridStatus(hass, energysite, coordinator))

async_add_entities(entities, True)
async_add_entities(entities, update_before_add=True)


class TeslaCarParkingBrake(TeslaCarEntity, BinarySensorEntity):
Expand Down
10 changes: 6 additions & 4 deletions custom_components/tesla_custom/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@

async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entities):
"""Set up the Tesla selects by config_entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"]
cars = hass.data[DOMAIN][config_entry.entry_id]["cars"]
entry_data = hass.data[DOMAIN][config_entry.entry_id]
coordinators = entry_data["coordinators"]
cars = entry_data["cars"]
entities = []

for car in cars.values():
for vin, car in cars.items():
coordinator = coordinators[vin]
entities.append(TeslaCarHorn(hass, car, coordinator))
entities.append(TeslaCarFlashLights(hass, car, coordinator))
entities.append(TeslaCarWakeUp(hass, car, coordinator))
Expand All @@ -28,7 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entitie
entities.append(TeslaCarRemoteStart(hass, car, coordinator))
entities.append(TeslaCarEmissionsTest(hass, car, coordinator))

async_add_entities(entities, True)
async_add_entities(entities, update_before_add=True)


class TeslaCarHorn(TeslaCarEntity, ButtonEntity):
Expand Down
11 changes: 6 additions & 5 deletions custom_components/tesla_custom/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,19 @@ async def async_setup_entry(
hass: HomeAssistant, config_entry, async_add_entities
) -> None:
"""Set up the Tesla climate by config_entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"]
cars = hass.data[DOMAIN][config_entry.entry_id]["cars"]
entry_data = hass.data[DOMAIN][config_entry.entry_id]
coordinators = entry_data["coordinators"]
cars = entry_data["cars"]

entities = [
TeslaCarClimate(
hass,
car,
coordinator,
coordinators[vin],
)
for car in cars.values()
for vin, car in cars.items()
]
async_add_entities(entities, True)
async_add_entities(entities, update_before_add=True)


class TeslaCarClimate(TeslaCarEntity, ClimateEntity):
Expand Down
10 changes: 6 additions & 4 deletions custom_components/tesla_custom/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,19 @@

async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entities):
"""Set up the Tesla locks by config_entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"]
cars = hass.data[DOMAIN][config_entry.entry_id]["cars"]
entry_data = hass.data[DOMAIN][config_entry.entry_id]
coordinators = entry_data["coordinators"]
cars = entry_data["cars"]
entities = []

for car in cars.values():
for vin, car in cars.items():
coordinator = coordinators[vin]
entities.append(TeslaCarChargerDoor(hass, car, coordinator))
entities.append(TeslaCarFrunk(hass, car, coordinator))
entities.append(TeslaCarTrunk(hass, car, coordinator))
entities.append(TeslaCarWindows(hass, car, coordinator))

async_add_entities(entities, True)
async_add_entities(entities, update_before_add=True)


class TeslaCarChargerDoor(TeslaCarEntity, CoverEntity):
Expand Down
10 changes: 6 additions & 4 deletions custom_components/tesla_custom/device_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,17 @@

async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entities):
"""Set up the Tesla device trackers by config_entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"]
cars = hass.data[DOMAIN][config_entry.entry_id]["cars"]
entry_data = hass.data[DOMAIN][config_entry.entry_id]
coordinators = entry_data["coordinators"]
cars = entry_data["cars"]
entities = []

for car in cars.values():
for vin, car in cars.items():
coordinator = coordinators[vin]
entities.append(TeslaCarLocation(hass, car, coordinator))
entities.append(TeslaCarDestinationLocation(hass, car, coordinator))

async_add_entities(entities, True)
async_add_entities(entities, update_before_add=True)


class TeslaCarLocation(TeslaCarEntity, TrackerEntity):
Expand Down
10 changes: 6 additions & 4 deletions custom_components/tesla_custom/lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,17 @@

async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entities):
"""Set up the Tesla locks by config_entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"]
cars = hass.data[DOMAIN][config_entry.entry_id]["cars"]
entry_data = hass.data[DOMAIN][config_entry.entry_id]
coordinators = entry_data["coordinators"]
cars = entry_data["cars"]
entities = []

for car in cars.values():
for vin, car in cars.items():
coordinator = coordinators[vin]
entities.append(TeslaCarDoors(hass, car, coordinator))
entities.append(TeslaCarChargePortLatch(hass, car, coordinator))

async_add_entities(entities, True)
async_add_entities(entities, update_before_add=True)


class TeslaCarDoors(TeslaCarEntity, LockEntity):
Expand Down
2 changes: 1 addition & 1 deletion custom_components/tesla_custom/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/alandtse/tesla/issues",
"loggers": ["teslajsonpy"],
"requirements": ["teslajsonpy==3.7.5"],
"requirements": ["teslajsonpy==3.8.0"],
"version": "3.10.4"
}
15 changes: 9 additions & 6 deletions custom_components/tesla_custom/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,23 @@

async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entities):
"""Set up the Tesla numbers by config_entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"]
cars = hass.data[DOMAIN][config_entry.entry_id]["cars"]
energysites = hass.data[DOMAIN][config_entry.entry_id]["energysites"]
entry_data = hass.data[DOMAIN][config_entry.entry_id]
coordinators = entry_data["coordinators"]
cars = entry_data["cars"]
energysites = entry_data["energysites"]
entities = []

for car in cars.values():
for vin, car in cars.items():
coordinator = coordinators[vin]
entities.append(TeslaCarChargeLimit(hass, car, coordinator))
entities.append(TeslaCarChargingAmps(hass, car, coordinator))

for energysite in energysites.values():
for energy_site_id, energysite in energysites.items():
coordinator = coordinators[energy_site_id]
if energysite.resource_type == RESOURCE_TYPE_BATTERY:
entities.append(TeslaEnergyBackupReserve(hass, energysite, coordinator))

async_add_entities(entities, True)
async_add_entities(entities, update_before_add=True)


class TeslaCarChargeLimit(TeslaCarEntity, NumberEntity):
Expand Down
15 changes: 9 additions & 6 deletions custom_components/tesla_custom/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,14 @@

async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entities):
"""Set up the Tesla selects by config_entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]["coordinator"]
cars = hass.data[DOMAIN][config_entry.entry_id]["cars"]
energysites = hass.data[DOMAIN][config_entry.entry_id]["energysites"]
entry_data = hass.data[DOMAIN][config_entry.entry_id]
coordinators = entry_data["coordinators"]
cars = entry_data["cars"]
energysites = entry_data["energysites"]
entities = []

for car in cars.values():
for vin, car in cars.items():
coordinator = coordinators[vin]
entities.append(TeslaCarCabinOverheatProtection(hass, car, coordinator))
for seat_name in SEAT_ID_MAP:
if "rear" in seat_name and not car.rear_seat_heaters:
Expand All @@ -87,14 +89,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entitie
continue
entities.append(TeslaCarHeatedSeat(hass, car, coordinator, seat_name))

for energysite in energysites.values():
for energy_site_id, energysite in energysites.items():
coordinator = coordinators[energy_site_id]
if energysite.resource_type == RESOURCE_TYPE_BATTERY:
entities.append(TeslaEnergyOperationMode(hass, energysite, coordinator))
if energysite.resource_type == RESOURCE_TYPE_BATTERY and energysite.has_solar:
entities.append(TeslaEnergyExportRule(hass, energysite, coordinator))
entities.append(TeslaEnergyGridCharging(hass, energysite, coordinator))

async_add_entities(entities, True)
async_add_entities(entities, update_before_add=True)


class TeslaCarHeatedSeat(TeslaCarEntity, SelectEntity):
Expand Down
Loading

0 comments on commit 41dfbcc

Please sign in to comment.