Skip to content

Commit

Permalink
Add DataUpdateCoordinator (home-assistant/core/pull/67003)
Browse files Browse the repository at this point in the history
  • Loading branch information
rikroe committed Mar 27, 2022
1 parent 03b9fe0 commit 8e32fdb
Show file tree
Hide file tree
Showing 10 changed files with 294 additions and 367 deletions.
301 changes: 55 additions & 246 deletions custom_components/bmw_connected_drive/__init__.py

Large diffs are not rendered by default.

35 changes: 15 additions & 20 deletions custom_components/bmw_connected_drive/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,9 @@
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.unit_system import UnitSystem

from . import (
DOMAIN as BMW_DOMAIN,
BMWConnectedDriveAccount,
BMWConnectedDriveBaseEntity,
)
from .const import CONF_ACCOUNT, DATA_ENTRIES, UNIT_MAP
from . import BMWConnectedDriveBaseEntity
from .const import DOMAIN, UNIT_MAP
from .coordinator import BMWDataUpdateCoordinator

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -216,17 +213,15 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the BMW ConnectedDrive binary sensors from config entry."""
account: BMWConnectedDriveAccount = hass.data[BMW_DOMAIN][DATA_ENTRIES][
config_entry.entry_id
][CONF_ACCOUNT]
coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]

entities = [
BMWConnectedDriveSensor(account, vehicle, description, hass.config.units)
for vehicle in account.account.vehicles
BMWConnectedDriveSensor(coordinator, vehicle, description, hass.config.units)
for vehicle in coordinator.account.vehicles
for description in SENSOR_TYPES
if description.key in vehicle.available_attributes
]
async_add_entities(entities, True)
async_add_entities(entities)


class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, BinarySensorEntity):
Expand All @@ -236,26 +231,26 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, BinarySensorEntity):

def __init__(
self,
account: BMWConnectedDriveAccount,
coordinator: BMWDataUpdateCoordinator,
vehicle: ConnectedDriveVehicle,
description: BMWBinarySensorEntityDescription,
unit_system: UnitSystem,
) -> None:
"""Initialize sensor."""
super().__init__(account, vehicle)
super().__init__(coordinator, vehicle)
self.entity_description = description
self._unit_system = unit_system

self._attr_name = f"{vehicle.name} {description.key}"
self._attr_unique_id = f"{vehicle.vin}-{description.key}"

def update(self) -> None:
"""Read new state data from the library."""
_LOGGER.debug("Updating binary sensors of %s", self._vehicle.name)
vehicle_state = self._vehicle.status
@property
def is_on(self) -> bool:
"""Return true if the binary sensor is on."""
_LOGGER.debug("Updating binary sensors of %s", self.vehicle.name)
vehicle_state = self.vehicle.status
result = self._attrs.copy()

self._attr_is_on = self.entity_description.value_fn(
return self.entity_description.value_fn(
vehicle_state, result, self._unit_system
)
self._attr_extra_state_attributes = result
45 changes: 23 additions & 22 deletions custom_components/bmw_connected_drive/button.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"""Support for BMW connected drive button entities."""
from __future__ import annotations

from collections.abc import Callable
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from typing import TYPE_CHECKING

from bimmer_connected.remote_services import RemoteServiceStatus
from bimmer_connected.vehicle import ConnectedDriveVehicle
Expand All @@ -12,12 +13,11 @@
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from . import (
DOMAIN as BMW_DOMAIN,
BMWConnectedDriveAccount,
BMWConnectedDriveBaseEntity,
)
from .const import CONF_ACCOUNT, DATA_ENTRIES
from . import BMWConnectedDriveBaseEntity
from .const import DOMAIN

if TYPE_CHECKING:
from .coordinator import BMWDataUpdateCoordinator


@dataclass
Expand All @@ -28,7 +28,7 @@ class BMWButtonEntityDescription(ButtonEntityDescription):
remote_function: Callable[
[ConnectedDriveVehicle], RemoteServiceStatus
] | None = None
account_function: Callable[[BMWConnectedDriveAccount], None] | None = None
account_function: Callable[[BMWDataUpdateCoordinator], Coroutine] | None = None


BUTTON_TYPES: tuple[BMWButtonEntityDescription, ...] = (
Expand Down Expand Up @@ -66,7 +66,7 @@ class BMWButtonEntityDescription(ButtonEntityDescription):
key="refresh",
icon="mdi:refresh",
name="Refresh from cloud",
account_function=lambda account: account.update(),
account_function=lambda coordinator: coordinator.async_request_refresh(),
enabled_when_read_only=True,
),
)
Expand All @@ -78,18 +78,17 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the BMW ConnectedDrive buttons from config entry."""
account: BMWConnectedDriveAccount = hass.data[BMW_DOMAIN][DATA_ENTRIES][
config_entry.entry_id
][CONF_ACCOUNT]
coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]

entities: list[BMWButton] = []

for vehicle in account.account.vehicles:
for vehicle in coordinator.account.vehicles:
entities.extend(
[
BMWButton(account, vehicle, description)
BMWButton(coordinator, vehicle, description)
for description in BUTTON_TYPES
if not account.read_only
or (account.read_only and description.enabled_when_read_only)
if not coordinator.read_only
or (coordinator.read_only and description.enabled_when_read_only)
]
)

Expand All @@ -103,20 +102,22 @@ class BMWButton(BMWConnectedDriveBaseEntity, ButtonEntity):

def __init__(
self,
account: BMWConnectedDriveAccount,
coordinator: BMWDataUpdateCoordinator,
vehicle: ConnectedDriveVehicle,
description: BMWButtonEntityDescription,
) -> None:
"""Initialize BMW vehicle sensor."""
super().__init__(account, vehicle)
super().__init__(coordinator, vehicle)
self.entity_description = description

self._attr_name = f"{vehicle.name} {description.name}"
self._attr_unique_id = f"{vehicle.vin}-{description.key}"

def press(self) -> None:
"""Process the button press."""
async def async_press(self) -> None:
"""Press the button."""
if self.entity_description.remote_function:
self.entity_description.remote_function(self._vehicle)
await self.hass.async_add_executor_job(
self.entity_description.remote_function(self.vehicle)
)
elif self.entity_description.account_function:
self.entity_description.account_function(self._account)
await self.entity_description.account_function(self.coordinator)
4 changes: 0 additions & 4 deletions custom_components/bmw_connected_drive/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,6 @@ async def async_step_user(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)

async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult:
"""Handle import."""
return await self.async_step_user(user_input)

@staticmethod
@callback
def async_get_options_flow(
Expand Down
13 changes: 10 additions & 3 deletions custom_components/bmw_connected_drive/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,29 @@
VOLUME_LITERS,
)

DOMAIN = "bmw_connected_drive"
ATTRIBUTION = "Data provided by BMW Connected Drive"

ATTR_DIRECTION = "direction"
ATTR_VIN = "vin"

CONF_ALLOWED_REGIONS = ["china", "north_america", "rest_of_world"]
CONF_READ_ONLY = "read_only"
CONF_USE_LOCATION = "use_location"

CONF_ACCOUNT = "account"

DATA_HASS_CONFIG = "hass_config"
DATA_ENTRIES = "entries"

UNIT_MAP = {
"KILOMETERS": LENGTH_KILOMETERS,
"MILES": LENGTH_MILES,
"LITERS": VOLUME_LITERS,
"GALLONS": VOLUME_GALLONS,
}

SERVICE_MAP = {
"light_flash": "trigger_remote_light_flash",
"sound_horn": "trigger_remote_horn",
"activate_air_conditioning": "trigger_remote_air_conditioning",
"deactivate_air_conditioning": "trigger_remote_air_conditioning_stop",
"find_vehicle": "trigger_remote_vehicle_finder",
}
131 changes: 131 additions & 0 deletions custom_components/bmw_connected_drive/coordinator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""Coordinator for BMW."""
from __future__ import annotations

from datetime import timedelta
import logging
from typing import Any, cast

import async_timeout
from bimmer_connected.account import ConnectedDriveAccount
from bimmer_connected.country_selector import get_region_from_name
from bimmer_connected.vehicle import ConnectedDriveVehicle

from homeassistant.const import CONF_DEVICE_ID
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import device_registry
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import ATTR_VIN, DOMAIN, SERVICE_MAP

SCAN_INTERVAL = timedelta(seconds=300)
_LOGGER = logging.getLogger(__name__)


class BMWDataUpdateCoordinator(DataUpdateCoordinator):
"""Class to manage fetching BMW data."""

account: ConnectedDriveAccount

def __init__(
self,
hass: HomeAssistant,
*,
username: str,
password: str,
region: str,
read_only: bool = False,
) -> None:
"""Initialize account-wide BMW data updater."""
# Storing username & password in coordinator is needed until a new library version
# that does not do blocking IO on init.
self._username = username
self._password = password
self._region = get_region_from_name(region)

self.account = None
self.read_only = read_only

super().__init__(
hass,
_LOGGER,
name=f"{DOMAIN}-{username}",
update_interval=SCAN_INTERVAL,
)

async def _async_update_data(self) -> None:
"""Fetch data from BMW."""
try:
async with async_timeout.timeout(15):
if isinstance(self.account, ConnectedDriveAccount):
# pylint: disable=protected-access
await self.hass.async_add_executor_job(self.account._get_vehicles)
else:
self.account = await self.hass.async_add_executor_job(
ConnectedDriveAccount,
self._username,
self._password,
self._region,
)
except OSError as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err

# Deprecated and will be removed in 2022.4 when only buttons are supported.
# Required to call `async_request_refresh` from a service call with arguments.
async def async_request_refresh(self, *args: Any, **kwargs: Any) -> None:
"""Request a refresh.
Refresh will wait a bit to see if it can batch them.
Allows to be called from a service call.
"""
await super().async_request_refresh()

# Deprecated and will be removed in 2022.4 when only buttons are supported.
async def async_execute_service(self, call: ServiceCall) -> None:
"""Execute a service for a vehicle."""
_LOGGER.warning(
"BMW Connected Drive services are deprecated. Please migrate to the dedicated button entities. "
"See https://www.home-assistant.io/integrations/bmw_connected_drive/#buttons for details"
)

vin: str | None = call.data.get(ATTR_VIN)
device_id: str | None = call.data.get(CONF_DEVICE_ID)

coordinator: BMWDataUpdateCoordinator
vehicle: ConnectedDriveVehicle | None = None

if not vin and device_id:
# If vin is None, device_id must be set (given by SERVICE_SCHEMA)
if not (
device := device_registry.async_get(self.hass).async_get(device_id)
):
_LOGGER.error("Could not find a device for id: %s", device_id)
return
vin = next(iter(device.identifiers))[1]
else:
vin = cast(str, vin)

# Search through all coordinators for vehicle
# Double check for read_only accounts as another account could create the services
entry_coordinator: BMWDataUpdateCoordinator
for entry_coordinator in self.hass.data[DOMAIN].values():
if (
isinstance(entry_coordinator, DataUpdateCoordinator)
and not entry_coordinator.read_only
):
account: ConnectedDriveAccount = entry_coordinator.account
if vehicle := account.get_vehicle(vin):
coordinator = entry_coordinator
break
if not vehicle:
_LOGGER.error("Could not find a vehicle for VIN %s", vin)
return
function_name = SERVICE_MAP[call.service]
function_call = getattr(vehicle.remote_services, function_name)
await self.hass.async_add_executor_job(function_call)

if call.service in [
"find_vehicle",
"activate_air_conditioning",
"deactivate_air_conditioning",
]:
await coordinator.async_request_refresh()
Loading

0 comments on commit 8e32fdb

Please sign in to comment.