Add DataUpdateCoordinator to bmw_connected_drive (#67003)

Co-authored-by: rikroe <rikroe@users.noreply.github.com>
This commit is contained in:
rikroe 2022-04-21 10:13:09 +02:00 committed by GitHub
parent 089f7279bc
commit 80653463bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 344 additions and 411 deletions

View File

@ -118,6 +118,7 @@ omit =
homeassistant/components/bmw_connected_drive/__init__.py homeassistant/components/bmw_connected_drive/__init__.py
homeassistant/components/bmw_connected_drive/binary_sensor.py homeassistant/components/bmw_connected_drive/binary_sensor.py
homeassistant/components/bmw_connected_drive/button.py homeassistant/components/bmw_connected_drive/button.py
homeassistant/components/bmw_connected_drive/coordinator.py
homeassistant/components/bmw_connected_drive/device_tracker.py homeassistant/components/bmw_connected_drive/device_tracker.py
homeassistant/components/bmw_connected_drive/lock.py homeassistant/components/bmw_connected_drive/lock.py
homeassistant/components/bmw_connected_drive/notify.py homeassistant/components/bmw_connected_drive/notify.py

View File

@ -1,47 +1,30 @@
"""Reads vehicle status from BMW connected drive portal.""" """Reads vehicle status from BMW connected drive portal."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable
import logging
from typing import Any from typing import Any
from bimmer_connected.account import ConnectedDriveAccount
from bimmer_connected.country_selector import get_region_from_name
from bimmer_connected.vehicle import ConnectedDriveVehicle from bimmer_connected.vehicle import ConnectedDriveVehicle
import voluptuous as vol import voluptuous as vol
from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_DEVICE_ID, CONF_DEVICE_ID,
CONF_ENTITY_ID,
CONF_NAME, CONF_NAME,
CONF_PASSWORD, CONF_PASSWORD,
CONF_REGION, CONF_REGION,
CONF_USERNAME, CONF_USERNAME,
Platform, Platform,
) )
from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import discovery from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.entity import DeviceInfo, Entity
from homeassistant.helpers.event import track_utc_time_change
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.util import slugify from homeassistant.helpers.update_coordinator import CoordinatorEntity
import homeassistant.util.dt as dt_util
from .const import ( from .const import ATTR_VIN, ATTRIBUTION, CONF_READ_ONLY, DATA_HASS_CONFIG, DOMAIN
ATTRIBUTION, from .coordinator import BMWDataUpdateCoordinator
CONF_ACCOUNT,
CONF_READ_ONLY,
DATA_ENTRIES,
DATA_HASS_CONFIG,
)
_LOGGER = logging.getLogger(__name__)
DOMAIN = "bmw_connected_drive"
ATTR_VIN = "vin"
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False) CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
@ -64,12 +47,9 @@ PLATFORMS = [
Platform.NOTIFY, Platform.NOTIFY,
Platform.SENSOR, Platform.SENSOR,
] ]
UPDATE_INTERVAL = 5 # in minutes
SERVICE_UPDATE_STATE = "update_state" SERVICE_UPDATE_STATE = "update_state"
UNDO_UPDATE_LISTENER = "undo_update_listener"
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the BMW Connected Drive component from configuration.yaml.""" """Set up the BMW Connected Drive component from configuration.yaml."""
@ -96,37 +76,23 @@ def _async_migrate_options_from_data_if_missing(
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up BMW Connected Drive from a config entry.""" """Set up BMW Connected Drive from a config entry."""
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN].setdefault(DATA_ENTRIES, {})
_async_migrate_options_from_data_if_missing(hass, entry) _async_migrate_options_from_data_if_missing(hass, entry)
try: # Set up one data coordinator per account/config entry
account = await hass.async_add_executor_job( coordinator = BMWDataUpdateCoordinator(
setup_account, entry, hass, entry.data[CONF_USERNAME] hass,
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
region=entry.data[CONF_REGION],
read_only=entry.options[CONF_READ_ONLY],
) )
except OSError as ex: await coordinator.async_config_entry_first_refresh()
raise ConfigEntryNotReady from ex
async def _async_update_all(service_call: ServiceCall | None = None) -> None: hass.data.setdefault(DOMAIN, {})
"""Update all BMW accounts.""" hass.data[DOMAIN][entry.entry_id] = coordinator
await hass.async_add_executor_job(_update_all)
def _update_all() -> None:
"""Update all BMW accounts."""
for entry in hass.data[DOMAIN][DATA_ENTRIES].copy().values():
entry[CONF_ACCOUNT].update()
# Add update listener for config entry changes (options)
undo_listener = entry.add_update_listener(update_listener)
hass.data[DOMAIN][DATA_ENTRIES][entry.entry_id] = {
CONF_ACCOUNT: account,
UNDO_UPDATE_LISTENER: undo_listener,
}
await _async_update_all()
# Set up all platforms except notify
hass.config_entries.async_setup_platforms( hass.config_entries.async_setup_platforms(
entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY] entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY]
) )
@ -138,11 +104,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass, hass,
Platform.NOTIFY, Platform.NOTIFY,
DOMAIN, DOMAIN,
{CONF_NAME: DOMAIN}, {CONF_NAME: DOMAIN, CONF_ENTITY_ID: entry.entry_id},
hass.data[DOMAIN][DATA_HASS_CONFIG], hass.data[DOMAIN][DATA_HASS_CONFIG],
) )
) )
# Add event listener for option flow changes
entry.async_on_unload(entry.add_update_listener(async_update_options))
return True return True
@ -152,140 +121,45 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY] entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY]
) )
for vehicle in hass.data[DOMAIN][DATA_ENTRIES][entry.entry_id][
CONF_ACCOUNT
].account.vehicles:
hass.services.async_remove(NOTIFY_DOMAIN, slugify(f"{DOMAIN}_{vehicle.name}"))
if unload_ok: if unload_ok:
hass.data[DOMAIN][DATA_ENTRIES][entry.entry_id][UNDO_UPDATE_LISTENER]() hass.data[DOMAIN].pop(entry.entry_id)
hass.data[DOMAIN][DATA_ENTRIES].pop(entry.entry_id)
return unload_ok return unload_ok
async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None: async def async_update_options(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Handle options update.""" """Handle options update."""
await hass.config_entries.async_reload(config_entry.entry_id) await hass.config_entries.async_reload(config_entry.entry_id)
def setup_account( class BMWConnectedDriveBaseEntity(CoordinatorEntity, Entity):
entry: ConfigEntry, hass: HomeAssistant, name: str
) -> BMWConnectedDriveAccount:
"""Set up a new BMWConnectedDriveAccount based on the config."""
username: str = entry.data[CONF_USERNAME]
password: str = entry.data[CONF_PASSWORD]
region: str = entry.data[CONF_REGION]
read_only: bool = entry.options[CONF_READ_ONLY]
_LOGGER.debug("Adding new account %s", name)
pos = (hass.config.latitude, hass.config.longitude)
cd_account = BMWConnectedDriveAccount(
username, password, region, name, read_only, *pos
)
# update every UPDATE_INTERVAL minutes, starting now
# this should even out the load on the servers
now = dt_util.utcnow()
track_utc_time_change(
hass,
cd_account.update,
minute=range(now.minute % UPDATE_INTERVAL, 60, UPDATE_INTERVAL),
second=now.second,
)
# Initialize
cd_account.update()
return cd_account
class BMWConnectedDriveAccount:
"""Representation of a BMW vehicle."""
def __init__(
self,
username: str,
password: str,
region_str: str,
name: str,
read_only: bool,
lat: float | None = None,
lon: float | None = None,
) -> None:
"""Initialize account."""
region = get_region_from_name(region_str)
self.read_only = read_only
self.account = ConnectedDriveAccount(username, password, region)
self.name = name
self._update_listeners: list[Callable[[], None]] = []
# Set observer position once for older cars to be in range for
# GPS position (pre-7/2014, <2km) and get new data from API
if lat and lon:
self.account.set_observer_position(lat, lon)
self.account.update_vehicle_states()
def update(self, *_: Any) -> None:
"""Update the state of all vehicles.
Notify all listeners about the update.
"""
_LOGGER.debug(
"Updating vehicle state for account %s, notifying %d listeners",
self.name,
len(self._update_listeners),
)
try:
self.account.update_vehicle_states()
for listener in self._update_listeners:
listener()
except OSError as exception:
_LOGGER.error(
"Could not connect to the BMW Connected Drive portal. "
"The vehicle state could not be updated"
)
_LOGGER.exception(exception)
def add_update_listener(self, listener: Callable[[], None]) -> None:
"""Add a listener for update notifications."""
self._update_listeners.append(listener)
class BMWConnectedDriveBaseEntity(Entity):
"""Common base for BMW entities.""" """Common base for BMW entities."""
_attr_should_poll = False coordinator: BMWDataUpdateCoordinator
_attr_attribution = ATTRIBUTION _attr_attribution = ATTRIBUTION
def __init__( def __init__(
self, self,
account: BMWConnectedDriveAccount, coordinator: BMWDataUpdateCoordinator,
vehicle: ConnectedDriveVehicle, vehicle: ConnectedDriveVehicle,
) -> None: ) -> None:
"""Initialize sensor.""" """Initialize entity."""
self._account = account super().__init__(coordinator)
self._vehicle = vehicle
self.vehicle = vehicle
self._attrs: dict[str, Any] = { self._attrs: dict[str, Any] = {
"car": self._vehicle.name, "car": self.vehicle.name,
"vin": self._vehicle.vin, "vin": self.vehicle.vin,
} }
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, vehicle.vin)}, identifiers={(DOMAIN, self.vehicle.vin)},
manufacturer=vehicle.brand.name, manufacturer=vehicle.brand.name,
model=vehicle.name, model=vehicle.name,
name=f"{vehicle.brand.name} {vehicle.name}", name=f"{vehicle.brand.name} {vehicle.name}",
) )
def update_callback(self) -> None:
"""Schedule a state update."""
self.schedule_update_ha_state(True)
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Add callback after being added to hass. """When entity is added to hass."""
await super().async_added_to_hass()
Show latest data after startup. self._handle_coordinator_update()
"""
self._account.add_update_listener(self.update_callback)

View File

@ -20,100 +20,37 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntityDescription, BinarySensorEntityDescription,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.unit_system import UnitSystem from homeassistant.util.unit_system import UnitSystem
from . import ( from . import BMWConnectedDriveBaseEntity
DOMAIN as BMW_DOMAIN, from .const import DOMAIN, UNIT_MAP
BMWConnectedDriveAccount, from .coordinator import BMWDataUpdateCoordinator
BMWConnectedDriveBaseEntity,
)
from .const import CONF_ACCOUNT, DATA_ENTRIES, UNIT_MAP
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def _are_doors_closed( def _condition_based_services(
vehicle_state: VehicleStatus, extra_attributes: dict[str, Any], *args: Any vehicle_state: VehicleStatus, unit_system: UnitSystem
) -> bool: ) -> dict[str, Any]:
# device class opening: On means open, Off means closed extra_attributes = {}
_LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed)
for lid in vehicle_state.lids:
extra_attributes[lid.name] = lid.state.value
return not vehicle_state.all_lids_closed
def _are_windows_closed(
vehicle_state: VehicleStatus, extra_attributes: dict[str, Any], *args: Any
) -> bool:
# device class opening: On means open, Off means closed
for window in vehicle_state.windows:
extra_attributes[window.name] = window.state.value
return not vehicle_state.all_windows_closed
def _are_doors_locked(
vehicle_state: VehicleStatus, extra_attributes: dict[str, Any], *args: Any
) -> bool:
# device class lock: On means unlocked, Off means locked
# Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED
extra_attributes["door_lock_state"] = vehicle_state.door_lock_state.value
extra_attributes["last_update_reason"] = vehicle_state.last_update_reason
return vehicle_state.door_lock_state not in {LockState.LOCKED, LockState.SECURED}
def _are_parking_lights_on(
vehicle_state: VehicleStatus, extra_attributes: dict[str, Any], *args: Any
) -> bool:
# device class light: On means light detected, Off means no light
extra_attributes["lights_parking"] = vehicle_state.parking_lights.value
return cast(bool, vehicle_state.are_parking_lights_on)
def _are_problems_detected(
vehicle_state: VehicleStatus,
extra_attributes: dict[str, Any],
unit_system: UnitSystem,
) -> bool:
# device class problem: On means problem detected, Off means no problem
for report in vehicle_state.condition_based_services: for report in vehicle_state.condition_based_services:
extra_attributes.update(_format_cbs_report(report, unit_system)) extra_attributes.update(_format_cbs_report(report, unit_system))
return not vehicle_state.are_all_cbs_ok return extra_attributes
def _check_control_messages( def _check_control_messages(vehicle_state: VehicleStatus) -> dict[str, Any]:
vehicle_state: VehicleStatus, extra_attributes: dict[str, Any], *args: Any extra_attributes: dict[str, Any] = {}
) -> bool: if vehicle_state.has_check_control_messages:
# device class problem: On means problem detected, Off means no problem cbs_list = [
check_control_messages = vehicle_state.check_control_messages message.description_short
has_check_control_messages = vehicle_state.has_check_control_messages for message in vehicle_state.check_control_messages
if has_check_control_messages: ]
cbs_list = [message.description_short for message in check_control_messages]
extra_attributes["check_control_messages"] = cbs_list extra_attributes["check_control_messages"] = cbs_list
else: else:
extra_attributes["check_control_messages"] = "OK" extra_attributes["check_control_messages"] = "OK"
return cast(bool, vehicle_state.has_check_control_messages) return extra_attributes
def _is_vehicle_charging(
vehicle_state: VehicleStatus, extra_attributes: dict[str, Any], *args: Any
) -> bool:
# device class power: On means power detected, Off means no power
extra_attributes["charging_status"] = vehicle_state.charging_status.value
extra_attributes[
"last_charging_end_result"
] = vehicle_state.last_charging_end_result
return cast(bool, vehicle_state.charging_status == ChargingState.CHARGING)
def _is_vehicle_plugged_in(
vehicle_state: VehicleStatus, extra_attributes: dict[str, Any], *args: Any
) -> bool:
# device class plug: On means device is plugged in,
# Off means device is unplugged
extra_attributes["connection_status"] = vehicle_state.connection_status
return cast(str, vehicle_state.connection_status) == "CONNECTED"
def _format_cbs_report( def _format_cbs_report(
@ -139,7 +76,7 @@ def _format_cbs_report(
class BMWRequiredKeysMixin: class BMWRequiredKeysMixin:
"""Mixin for required keys.""" """Mixin for required keys."""
value_fn: Callable[[VehicleStatus, dict[str, Any], UnitSystem], bool] value_fn: Callable[[VehicleStatus], bool]
@dataclass @dataclass
@ -148,6 +85,8 @@ class BMWBinarySensorEntityDescription(
): ):
"""Describes BMW binary_sensor entity.""" """Describes BMW binary_sensor entity."""
attr_fn: Callable[[VehicleStatus, UnitSystem], dict[str, Any]] | None = None
SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = ( SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = (
BMWBinarySensorEntityDescription( BMWBinarySensorEntityDescription(
@ -155,42 +94,59 @@ SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = (
name="Doors", name="Doors",
device_class=BinarySensorDeviceClass.OPENING, device_class=BinarySensorDeviceClass.OPENING,
icon="mdi:car-door-lock", icon="mdi:car-door-lock",
value_fn=_are_doors_closed, # device class opening: On means open, Off means closed
value_fn=lambda s: not s.all_lids_closed,
attr_fn=lambda s, u: {lid.name: lid.state.value for lid in s.lids},
), ),
BMWBinarySensorEntityDescription( BMWBinarySensorEntityDescription(
key="windows", key="windows",
name="Windows", name="Windows",
device_class=BinarySensorDeviceClass.OPENING, device_class=BinarySensorDeviceClass.OPENING,
icon="mdi:car-door", icon="mdi:car-door",
value_fn=_are_windows_closed, # device class opening: On means open, Off means closed
value_fn=lambda s: not s.all_windows_closed,
attr_fn=lambda s, u: {window.name: window.state.value for window in s.windows},
), ),
BMWBinarySensorEntityDescription( BMWBinarySensorEntityDescription(
key="door_lock_state", key="door_lock_state",
name="Door lock state", name="Door lock state",
device_class=BinarySensorDeviceClass.LOCK, device_class=BinarySensorDeviceClass.LOCK,
icon="mdi:car-key", icon="mdi:car-key",
value_fn=_are_doors_locked, # device class lock: On means unlocked, Off means locked
# Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED
value_fn=lambda s: s.door_lock_state
not in {LockState.LOCKED, LockState.SECURED},
attr_fn=lambda s, u: {
"door_lock_state": s.door_lock_state.value,
"last_update_reason": s.last_update_reason,
},
), ),
BMWBinarySensorEntityDescription( BMWBinarySensorEntityDescription(
key="lights_parking", key="lights_parking",
name="Parking lights", name="Parking lights",
device_class=BinarySensorDeviceClass.LIGHT, device_class=BinarySensorDeviceClass.LIGHT,
icon="mdi:car-parking-lights", icon="mdi:car-parking-lights",
value_fn=_are_parking_lights_on, # device class light: On means light detected, Off means no light
value_fn=lambda s: cast(bool, s.are_parking_lights_on),
attr_fn=lambda s, u: {"lights_parking": s.parking_lights.value},
), ),
BMWBinarySensorEntityDescription( BMWBinarySensorEntityDescription(
key="condition_based_services", key="condition_based_services",
name="Condition based services", name="Condition based services",
device_class=BinarySensorDeviceClass.PROBLEM, device_class=BinarySensorDeviceClass.PROBLEM,
icon="mdi:wrench", icon="mdi:wrench",
value_fn=_are_problems_detected, # device class problem: On means problem detected, Off means no problem
value_fn=lambda s: not s.are_all_cbs_ok,
attr_fn=_condition_based_services,
), ),
BMWBinarySensorEntityDescription( BMWBinarySensorEntityDescription(
key="check_control_messages", key="check_control_messages",
name="Control messages", name="Control messages",
device_class=BinarySensorDeviceClass.PROBLEM, device_class=BinarySensorDeviceClass.PROBLEM,
icon="mdi:car-tire-alert", icon="mdi:car-tire-alert",
value_fn=_check_control_messages, # device class problem: On means problem detected, Off means no problem
value_fn=lambda s: cast(bool, s.has_check_control_messages),
attr_fn=lambda s, u: _check_control_messages(s),
), ),
# electric # electric
BMWBinarySensorEntityDescription( BMWBinarySensorEntityDescription(
@ -198,14 +154,20 @@ SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = (
name="Charging status", name="Charging status",
device_class=BinarySensorDeviceClass.BATTERY_CHARGING, device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
icon="mdi:ev-station", icon="mdi:ev-station",
value_fn=_is_vehicle_charging, # device class power: On means power detected, Off means no power
value_fn=lambda s: cast(bool, s.charging_status == ChargingState.CHARGING),
attr_fn=lambda s, u: {
"charging_status": s.charging_status.value,
"last_charging_end_result": s.last_charging_end_result,
},
), ),
BMWBinarySensorEntityDescription( BMWBinarySensorEntityDescription(
key="connection_status", key="connection_status",
name="Connection status", name="Connection status",
device_class=BinarySensorDeviceClass.PLUG, device_class=BinarySensorDeviceClass.PLUG,
icon="mdi:car-electric", icon="mdi:car-electric",
value_fn=_is_vehicle_plugged_in, value_fn=lambda s: cast(str, s.connection_status) == "CONNECTED",
attr_fn=lambda s, u: {"connection_status": s.connection_status},
), ),
) )
@ -216,17 +178,15 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the BMW ConnectedDrive binary sensors from config entry.""" """Set up the BMW ConnectedDrive binary sensors from config entry."""
account: BMWConnectedDriveAccount = hass.data[BMW_DOMAIN][DATA_ENTRIES][ coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
config_entry.entry_id
][CONF_ACCOUNT]
entities = [ entities = [
BMWConnectedDriveSensor(account, vehicle, description, hass.config.units) BMWConnectedDriveSensor(coordinator, vehicle, description, hass.config.units)
for vehicle in account.account.vehicles for vehicle in coordinator.account.vehicles
for description in SENSOR_TYPES for description in SENSOR_TYPES
if description.key in vehicle.available_attributes if description.key in vehicle.available_attributes
] ]
async_add_entities(entities, True) async_add_entities(entities)
class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, BinarySensorEntity): class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, BinarySensorEntity):
@ -236,26 +196,35 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, BinarySensorEntity):
def __init__( def __init__(
self, self,
account: BMWConnectedDriveAccount, coordinator: BMWDataUpdateCoordinator,
vehicle: ConnectedDriveVehicle, vehicle: ConnectedDriveVehicle,
description: BMWBinarySensorEntityDescription, description: BMWBinarySensorEntityDescription,
unit_system: UnitSystem, unit_system: UnitSystem,
) -> None: ) -> None:
"""Initialize sensor.""" """Initialize sensor."""
super().__init__(account, vehicle) super().__init__(coordinator, vehicle)
self.entity_description = description self.entity_description = description
self._unit_system = unit_system self._unit_system = unit_system
self._attr_name = f"{vehicle.name} {description.key}" self._attr_name = f"{vehicle.name} {description.key}"
self._attr_unique_id = f"{vehicle.vin}-{description.key}" self._attr_unique_id = f"{vehicle.vin}-{description.key}"
def update(self) -> None: @callback
"""Read new state data from the library.""" def _handle_coordinator_update(self) -> None:
_LOGGER.debug("Updating binary sensors of %s", self._vehicle.name) """Handle updated data from the coordinator."""
vehicle_state = self._vehicle.status _LOGGER.debug(
result = self._attrs.copy() "Updating binary sensor '%s' of %s",
self.entity_description.key,
self._attr_is_on = self.entity_description.value_fn( self.vehicle.name,
vehicle_state, result, self._unit_system
) )
self._attr_extra_state_attributes = result vehicle_state = self.vehicle.status
self._attr_is_on = self.entity_description.value_fn(vehicle_state)
if self.entity_description.attr_fn:
self._attr_extra_state_attributes = dict(
self._attrs,
**self.entity_description.attr_fn(vehicle_state, self._unit_system),
)
super()._handle_coordinator_update()

View File

@ -1,10 +1,11 @@
"""Support for BMW connected drive button entities.""" """Support for BMW connected drive button entities."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable, Coroutine
from dataclasses import dataclass from dataclasses import dataclass
import logging
from typing import TYPE_CHECKING
from bimmer_connected.remote_services import RemoteServiceStatus
from bimmer_connected.vehicle import ConnectedDriveVehicle from bimmer_connected.vehicle import ConnectedDriveVehicle
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
@ -12,12 +13,13 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import ( from . import BMWConnectedDriveBaseEntity
DOMAIN as BMW_DOMAIN, from .const import DOMAIN
BMWConnectedDriveAccount,
BMWConnectedDriveBaseEntity, if TYPE_CHECKING:
) from .coordinator import BMWDataUpdateCoordinator
from .const import CONF_ACCOUNT, DATA_ENTRIES
_LOGGER = logging.getLogger(__name__)
@dataclass @dataclass
@ -25,10 +27,8 @@ class BMWButtonEntityDescription(ButtonEntityDescription):
"""Class describing BMW button entities.""" """Class describing BMW button entities."""
enabled_when_read_only: bool = False enabled_when_read_only: bool = False
remote_function: Callable[ remote_function: str | None = None
[ConnectedDriveVehicle], RemoteServiceStatus account_function: Callable[[BMWDataUpdateCoordinator], Coroutine] | None = None
] | None = None
account_function: Callable[[BMWConnectedDriveAccount], None] | None = None
BUTTON_TYPES: tuple[BMWButtonEntityDescription, ...] = ( BUTTON_TYPES: tuple[BMWButtonEntityDescription, ...] = (
@ -36,37 +36,37 @@ BUTTON_TYPES: tuple[BMWButtonEntityDescription, ...] = (
key="light_flash", key="light_flash",
icon="mdi:car-light-alert", icon="mdi:car-light-alert",
name="Flash Lights", name="Flash Lights",
remote_function=lambda vehicle: vehicle.remote_services.trigger_remote_light_flash(), remote_function="trigger_remote_light_flash",
), ),
BMWButtonEntityDescription( BMWButtonEntityDescription(
key="sound_horn", key="sound_horn",
icon="mdi:bullhorn", icon="mdi:bullhorn",
name="Sound Horn", name="Sound Horn",
remote_function=lambda vehicle: vehicle.remote_services.trigger_remote_horn(), remote_function="trigger_remote_horn",
), ),
BMWButtonEntityDescription( BMWButtonEntityDescription(
key="activate_air_conditioning", key="activate_air_conditioning",
icon="mdi:hvac", icon="mdi:hvac",
name="Activate Air Conditioning", name="Activate Air Conditioning",
remote_function=lambda vehicle: vehicle.remote_services.trigger_remote_air_conditioning(), remote_function="trigger_remote_air_conditioning",
), ),
BMWButtonEntityDescription( BMWButtonEntityDescription(
key="deactivate_air_conditioning", key="deactivate_air_conditioning",
icon="mdi:hvac-off", icon="mdi:hvac-off",
name="Deactivate Air Conditioning", name="Deactivate Air Conditioning",
remote_function=lambda vehicle: vehicle.remote_services.trigger_remote_air_conditioning_stop(), remote_function="trigger_remote_air_conditioning_stop",
), ),
BMWButtonEntityDescription( BMWButtonEntityDescription(
key="find_vehicle", key="find_vehicle",
icon="mdi:crosshairs-question", icon="mdi:crosshairs-question",
name="Find Vehicle", name="Find Vehicle",
remote_function=lambda vehicle: vehicle.remote_services.trigger_remote_vehicle_finder(), remote_function="trigger_remote_vehicle_finder",
), ),
BMWButtonEntityDescription( BMWButtonEntityDescription(
key="refresh", key="refresh",
icon="mdi:refresh", icon="mdi:refresh",
name="Refresh from cloud", name="Refresh from cloud",
account_function=lambda account: account.update(), account_function=lambda coordinator: coordinator.async_request_refresh(),
enabled_when_read_only=True, enabled_when_read_only=True,
), ),
) )
@ -78,18 +78,17 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the BMW ConnectedDrive buttons from config entry.""" """Set up the BMW ConnectedDrive buttons from config entry."""
account: BMWConnectedDriveAccount = hass.data[BMW_DOMAIN][DATA_ENTRIES][ coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
config_entry.entry_id
][CONF_ACCOUNT]
entities: list[BMWButton] = [] entities: list[BMWButton] = []
for vehicle in account.account.vehicles: for vehicle in coordinator.account.vehicles:
entities.extend( entities.extend(
[ [
BMWButton(account, vehicle, description) BMWButton(coordinator, vehicle, description)
for description in BUTTON_TYPES for description in BUTTON_TYPES
if not account.read_only if not coordinator.read_only
or (account.read_only and description.enabled_when_read_only) or (coordinator.read_only and description.enabled_when_read_only)
] ]
) )
@ -103,20 +102,35 @@ class BMWButton(BMWConnectedDriveBaseEntity, ButtonEntity):
def __init__( def __init__(
self, self,
account: BMWConnectedDriveAccount, coordinator: BMWDataUpdateCoordinator,
vehicle: ConnectedDriveVehicle, vehicle: ConnectedDriveVehicle,
description: BMWButtonEntityDescription, description: BMWButtonEntityDescription,
) -> None: ) -> None:
"""Initialize BMW vehicle sensor.""" """Initialize BMW vehicle sensor."""
super().__init__(account, vehicle) super().__init__(coordinator, vehicle)
self.entity_description = description self.entity_description = description
self._attr_name = f"{vehicle.name} {description.name}" self._attr_name = f"{vehicle.name} {description.name}"
self._attr_unique_id = f"{vehicle.vin}-{description.key}" self._attr_unique_id = f"{vehicle.vin}-{description.key}"
def press(self) -> None: async def async_press(self) -> None:
"""Process the button press.""" """Press the button."""
if self.entity_description.remote_function: if self.entity_description.remote_function:
self.entity_description.remote_function(self._vehicle) await self.hass.async_add_executor_job(
getattr(
self.vehicle.remote_services,
self.entity_description.remote_function,
)
)
elif self.entity_description.account_function: elif self.entity_description.account_function:
self.entity_description.account_function(self._account) _LOGGER.warning(
"The 'Refresh from cloud' button is deprecated. Use the 'homeassistant.update_entity' "
"service with any BMW entity for a full reload. See https://www.home-assistant.io/"
"integrations/bmw_connected_drive/#update-the-state--refresh-from-api for details"
)
await self.entity_description.account_function(self.coordinator)
# Always update HA states after a button was executed.
# BMW remote services that change the vehicle's state update the local object
# when executing the service, so only the HA state machine needs further updates.
self.coordinator.notify_listeners()

View File

@ -6,17 +6,17 @@ from homeassistant.const import (
VOLUME_LITERS, VOLUME_LITERS,
) )
DOMAIN = "bmw_connected_drive"
ATTRIBUTION = "Data provided by BMW Connected Drive" ATTRIBUTION = "Data provided by BMW Connected Drive"
ATTR_DIRECTION = "direction" ATTR_DIRECTION = "direction"
ATTR_VIN = "vin"
CONF_ALLOWED_REGIONS = ["china", "north_america", "rest_of_world"] CONF_ALLOWED_REGIONS = ["china", "north_america", "rest_of_world"]
CONF_READ_ONLY = "read_only" CONF_READ_ONLY = "read_only"
CONF_ACCOUNT = "account" CONF_ACCOUNT = "account"
DATA_HASS_CONFIG = "hass_config" DATA_HASS_CONFIG = "hass_config"
DATA_ENTRIES = "entries"
UNIT_MAP = { UNIT_MAP = {
"KILOMETERS": LENGTH_KILOMETERS, "KILOMETERS": LENGTH_KILOMETERS,

View File

@ -0,0 +1,74 @@
"""Coordinator for BMW."""
from __future__ import annotations
from datetime import timedelta
import logging
import async_timeout
from bimmer_connected.account import ConnectedDriveAccount
from bimmer_connected.country_selector import get_region_from_name
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
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,
)
self.account.set_observer_position(
self.hass.config.latitude, self.hass.config.longitude
)
except OSError as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err
def notify_listeners(self) -> None:
"""Notify all listeners to refresh HA state machine."""
for update_callback in self._listeners:
update_callback()

View File

@ -12,12 +12,9 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import ( from . import BMWConnectedDriveBaseEntity
DOMAIN as BMW_DOMAIN, from .const import ATTR_DIRECTION, DOMAIN
BMWConnectedDriveAccount, from .coordinator import BMWDataUpdateCoordinator
BMWConnectedDriveBaseEntity,
)
from .const import ATTR_DIRECTION, CONF_ACCOUNT, DATA_ENTRIES
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -28,20 +25,18 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the BMW ConnectedDrive tracker from config entry.""" """Set up the BMW ConnectedDrive tracker from config entry."""
account: BMWConnectedDriveAccount = hass.data[BMW_DOMAIN][DATA_ENTRIES][ coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
config_entry.entry_id
][CONF_ACCOUNT]
entities: list[BMWDeviceTracker] = [] entities: list[BMWDeviceTracker] = []
for vehicle in account.account.vehicles: for vehicle in coordinator.account.vehicles:
entities.append(BMWDeviceTracker(account, vehicle)) entities.append(BMWDeviceTracker(coordinator, vehicle))
if not vehicle.is_vehicle_tracking_enabled: if not vehicle.is_vehicle_tracking_enabled:
_LOGGER.info( _LOGGER.info(
"Tracking is (currently) disabled for vehicle %s (%s), defaulting to unknown", "Tracking is (currently) disabled for vehicle %s (%s), defaulting to unknown",
vehicle.name, vehicle.name,
vehicle.vin, vehicle.vin,
) )
async_add_entities(entities, True) async_add_entities(entities)
class BMWDeviceTracker(BMWConnectedDriveBaseEntity, TrackerEntity): class BMWDeviceTracker(BMWConnectedDriveBaseEntity, TrackerEntity):
@ -52,39 +47,39 @@ class BMWDeviceTracker(BMWConnectedDriveBaseEntity, TrackerEntity):
def __init__( def __init__(
self, self,
account: BMWConnectedDriveAccount, coordinator: BMWDataUpdateCoordinator,
vehicle: ConnectedDriveVehicle, vehicle: ConnectedDriveVehicle,
) -> None: ) -> None:
"""Initialize the Tracker.""" """Initialize the Tracker."""
super().__init__(account, vehicle) super().__init__(coordinator, vehicle)
self._attr_unique_id = vehicle.vin self._attr_unique_id = vehicle.vin
self._location = pos if (pos := vehicle.status.gps_position) else None
self._attr_name = vehicle.name self._attr_name = vehicle.name
@property
def extra_state_attributes(self) -> dict:
"""Return entity specific state attributes."""
return dict(self._attrs, **{ATTR_DIRECTION: self.vehicle.status.gps_heading})
@property @property
def latitude(self) -> float | None: def latitude(self) -> float | None:
"""Return latitude value of the device.""" """Return latitude value of the device."""
return self._location[0] if self._location else None return (
self.vehicle.status.gps_position[0]
if self.vehicle.is_vehicle_tracking_enabled
else None
)
@property @property
def longitude(self) -> float | None: def longitude(self) -> float | None:
"""Return longitude value of the device.""" """Return longitude value of the device."""
return self._location[1] if self._location else None return (
self.vehicle.status.gps_position[1]
if self.vehicle.is_vehicle_tracking_enabled
else None
)
@property @property
def source_type(self) -> Literal["gps"]: def source_type(self) -> Literal["gps"]:
"""Return the source type, eg gps or router, of the device.""" """Return the source type, eg gps or router, of the device."""
return SOURCE_TYPE_GPS return SOURCE_TYPE_GPS
def update(self) -> None:
"""Update state of the device tracker."""
_LOGGER.debug("Updating device tracker of %s", self._vehicle.name)
state_attrs = self._attrs
state_attrs[ATTR_DIRECTION] = self._vehicle.status.gps_heading
self._attr_extra_state_attributes = state_attrs
self._location = (
self._vehicle.status.gps_position
if self._vehicle.is_vehicle_tracking_enabled
else None
)

View File

@ -1,4 +1,6 @@
"""Support for BMW car locks with BMW ConnectedDrive.""" """Support for BMW car locks with BMW ConnectedDrive."""
from __future__ import annotations
import logging import logging
from typing import Any from typing import Any
@ -7,15 +9,12 @@ from bimmer_connected.vehicle_status import LockState
from homeassistant.components.lock import LockEntity from homeassistant.components.lock import LockEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import ( from . import BMWConnectedDriveBaseEntity
DOMAIN as BMW_DOMAIN, from .const import DOMAIN
BMWConnectedDriveAccount, from .coordinator import BMWDataUpdateCoordinator
BMWConnectedDriveBaseEntity,
)
from .const import CONF_ACCOUNT, DATA_ENTRIES
DOOR_LOCK_STATE = "door_lock_state" DOOR_LOCK_STATE = "door_lock_state"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -27,16 +26,14 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the BMW ConnectedDrive binary sensors from config entry.""" """Set up the BMW ConnectedDrive binary sensors from config entry."""
account: BMWConnectedDriveAccount = hass.data[BMW_DOMAIN][DATA_ENTRIES][ coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
config_entry.entry_id
][CONF_ACCOUNT]
if not account.read_only: entities: list[BMWLock] = []
entities = [
BMWLock(account, vehicle, "lock", "BMW lock") for vehicle in coordinator.account.vehicles:
for vehicle in account.account.vehicles if not coordinator.read_only:
] entities.append(BMWLock(coordinator, vehicle, "lock", "BMW lock"))
async_add_entities(entities, True) async_add_entities(entities)
class BMWLock(BMWConnectedDriveBaseEntity, LockEntity): class BMWLock(BMWConnectedDriveBaseEntity, LockEntity):
@ -44,13 +41,13 @@ class BMWLock(BMWConnectedDriveBaseEntity, LockEntity):
def __init__( def __init__(
self, self,
account: BMWConnectedDriveAccount, coordinator: BMWDataUpdateCoordinator,
vehicle: ConnectedDriveVehicle, vehicle: ConnectedDriveVehicle,
attribute: str, attribute: str,
sensor_name: str, sensor_name: str,
) -> None: ) -> None:
"""Initialize the lock.""" """Initialize the lock."""
super().__init__(account, vehicle) super().__init__(coordinator, vehicle)
self._attribute = attribute self._attribute = attribute
self._attr_name = f"{vehicle.name} {attribute}" self._attr_name = f"{vehicle.name} {attribute}"
@ -60,38 +57,43 @@ class BMWLock(BMWConnectedDriveBaseEntity, LockEntity):
def lock(self, **kwargs: Any) -> None: def lock(self, **kwargs: Any) -> None:
"""Lock the car.""" """Lock the car."""
_LOGGER.debug("%s: locking doors", self._vehicle.name) _LOGGER.debug("%s: locking doors", self.vehicle.name)
# Only update the HA state machine if the vehicle reliably reports its lock state
if self.door_lock_state_available:
# Optimistic state set here because it takes some time before the # Optimistic state set here because it takes some time before the
# update callback response # update callback response
self._attr_is_locked = True self._attr_is_locked = True
self.schedule_update_ha_state() self.schedule_update_ha_state()
self._vehicle.remote_services.trigger_remote_door_lock() self.vehicle.remote_services.trigger_remote_door_lock()
def unlock(self, **kwargs: Any) -> None: def unlock(self, **kwargs: Any) -> None:
"""Unlock the car.""" """Unlock the car."""
_LOGGER.debug("%s: unlocking doors", self._vehicle.name) _LOGGER.debug("%s: unlocking doors", self.vehicle.name)
# Only update the HA state machine if the vehicle reliably reports its lock state
if self.door_lock_state_available:
# Optimistic state set here because it takes some time before the # Optimistic state set here because it takes some time before the
# update callback response # update callback response
self._attr_is_locked = False self._attr_is_locked = False
self.schedule_update_ha_state() self.schedule_update_ha_state()
self._vehicle.remote_services.trigger_remote_door_unlock() self.vehicle.remote_services.trigger_remote_door_unlock()
def update(self) -> None: @callback
"""Update state of the lock.""" def _handle_coordinator_update(self) -> None:
_LOGGER.debug( """Handle updated data from the coordinator."""
"Updating lock data for '%s' of %s", self._attribute, self._vehicle.name _LOGGER.debug("Updating lock data of %s", self.vehicle.name)
) # Only update the HA state machine if the vehicle reliably reports its lock state
vehicle_state = self._vehicle.status if self.door_lock_state_available:
if not self.door_lock_state_available: vehicle_state = self.vehicle.status
self._attr_is_locked = None
else:
self._attr_is_locked = vehicle_state.door_lock_state in { self._attr_is_locked = vehicle_state.door_lock_state in {
LockState.LOCKED, LockState.LOCKED,
LockState.SECURED, LockState.SECURED,
} }
self._attr_extra_state_attributes = dict(
self._attrs,
**{
"door_lock_state": vehicle_state.door_lock_state.value,
"last_update_reason": vehicle_state.last_update_reason,
},
)
result = self._attrs.copy() super()._handle_coordinator_update()
if self.door_lock_state_available:
result["door_lock_state"] = vehicle_state.door_lock_state.value
result["last_update_reason"] = vehicle_state.last_update_reason
self._attr_extra_state_attributes = result

View File

@ -11,12 +11,18 @@ from homeassistant.components.notify import (
ATTR_TARGET, ATTR_TARGET,
BaseNotificationService, BaseNotificationService,
) )
from homeassistant.const import ATTR_LATITUDE, ATTR_LOCATION, ATTR_LONGITUDE, ATTR_NAME from homeassistant.const import (
ATTR_LATITUDE,
ATTR_LOCATION,
ATTR_LONGITUDE,
ATTR_NAME,
CONF_ENTITY_ID,
)
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN as BMW_DOMAIN, BMWConnectedDriveAccount from .const import DOMAIN
from .const import CONF_ACCOUNT, DATA_ENTRIES from .coordinator import BMWDataUpdateCoordinator
ATTR_LAT = "lat" ATTR_LAT = "lat"
ATTR_LOCATION_ATTRIBUTES = ["street", "city", "postal_code", "country"] ATTR_LOCATION_ATTRIBUTES = ["street", "city", "postal_code", "country"]
@ -33,26 +39,22 @@ def get_service(
discovery_info: DiscoveryInfoType | None = None, discovery_info: DiscoveryInfoType | None = None,
) -> BMWNotificationService: ) -> BMWNotificationService:
"""Get the BMW notification service.""" """Get the BMW notification service."""
accounts: list[BMWConnectedDriveAccount] = [ coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][
e[CONF_ACCOUNT] for e in hass.data[BMW_DOMAIN][DATA_ENTRIES].values() (discovery_info or {})[CONF_ENTITY_ID]
] ]
_LOGGER.debug("Found BMW accounts: %s", ", ".join([a.name for a in accounts]))
svc = BMWNotificationService() targets = {}
svc.setup(accounts) if not coordinator.read_only:
return svc targets.update({v.name: v for v in coordinator.account.vehicles})
return BMWNotificationService(targets)
class BMWNotificationService(BaseNotificationService): class BMWNotificationService(BaseNotificationService):
"""Send Notifications to BMW.""" """Send Notifications to BMW."""
def __init__(self) -> None: def __init__(self, targets: dict[str, ConnectedDriveVehicle]) -> None:
"""Set up the notification service.""" """Set up the notification service."""
self.targets: dict[str, ConnectedDriveVehicle] = {} self.targets: dict[str, ConnectedDriveVehicle] = targets
def setup(self, accounts: list[BMWConnectedDriveAccount]) -> None:
"""Get the BMW vehicle(s) for the account(s)."""
for account in accounts:
self.targets.update({v.name: v for v in account.account.vehicles})
def send_message(self, message: str = "", **kwargs: Any) -> None: def send_message(self, message: str = "", **kwargs: Any) -> None:
"""Send a message or POI to the car.""" """Send a message or POI to the car."""

View File

@ -22,17 +22,14 @@ from homeassistant.const import (
VOLUME_GALLONS, VOLUME_GALLONS,
VOLUME_LITERS, VOLUME_LITERS,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
from homeassistant.util.unit_system import UnitSystem from homeassistant.util.unit_system import UnitSystem
from . import ( from . import BMWConnectedDriveBaseEntity
DOMAIN as BMW_DOMAIN, from .const import DOMAIN, UNIT_MAP
BMWConnectedDriveAccount, from .coordinator import BMWDataUpdateCoordinator
BMWConnectedDriveBaseEntity,
)
from .const import CONF_ACCOUNT, DATA_ENTRIES, UNIT_MAP
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -135,21 +132,20 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up the BMW ConnectedDrive sensors from config entry.""" """Set up the BMW ConnectedDrive sensors from config entry."""
unit_system = hass.config.units unit_system = hass.config.units
account: BMWConnectedDriveAccount = hass.data[BMW_DOMAIN][DATA_ENTRIES][ coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
config_entry.entry_id
][CONF_ACCOUNT]
entities: list[BMWConnectedDriveSensor] = [] entities: list[BMWConnectedDriveSensor] = []
for vehicle in account.account.vehicles: for vehicle in coordinator.account.vehicles:
entities.extend( entities.extend(
[ [
BMWConnectedDriveSensor(account, vehicle, description, unit_system) BMWConnectedDriveSensor(coordinator, vehicle, description, unit_system)
for attribute_name in vehicle.available_attributes for attribute_name in vehicle.available_attributes
if (description := SENSOR_TYPES.get(attribute_name)) if (description := SENSOR_TYPES.get(attribute_name))
] ]
) )
async_add_entities(entities, True) async_add_entities(entities)
class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity): class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity):
@ -159,13 +155,13 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity):
def __init__( def __init__(
self, self,
account: BMWConnectedDriveAccount, coordinator: BMWDataUpdateCoordinator,
vehicle: ConnectedDriveVehicle, vehicle: ConnectedDriveVehicle,
description: BMWSensorEntityDescription, description: BMWSensorEntityDescription,
unit_system: UnitSystem, unit_system: UnitSystem,
) -> None: ) -> None:
"""Initialize BMW vehicle sensor.""" """Initialize BMW vehicle sensor."""
super().__init__(account, vehicle) super().__init__(coordinator, vehicle)
self.entity_description = description self.entity_description = description
self._attr_name = f"{vehicle.name} {description.key}" self._attr_name = f"{vehicle.name} {description.key}"
@ -176,8 +172,14 @@ class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, SensorEntity):
else: else:
self._attr_native_unit_of_measurement = description.unit_metric self._attr_native_unit_of_measurement = description.unit_metric
@property @callback
def native_value(self) -> StateType: def _handle_coordinator_update(self) -> None:
"""Return the state.""" """Handle updated data from the coordinator."""
state = getattr(self._vehicle.status, self.entity_description.key) _LOGGER.debug(
return cast(StateType, self.entity_description.value(state, self.hass)) "Updating sensor '%s' of %s", self.entity_description.key, self.vehicle.name
)
state = getattr(self.vehicle.status, self.entity_description.key)
self._attr_native_value = cast(
StateType, self.entity_description.value(state, self.hass)
)
super()._handle_coordinator_update()