Skip to content

Commit

Permalink
Update to Home Assistant 2024.12.0b3 (#80)
Browse files Browse the repository at this point in the history
Co-authored-by: rikroe <[email protected]>
  • Loading branch information
github-actions[bot] and rikroe authored Dec 1, 2024
1 parent d5492c1 commit 4a3e6e8
Show file tree
Hide file tree
Showing 18 changed files with 919 additions and 476 deletions.
112 changes: 50 additions & 62 deletions custom_components/bmw_connected_drive/__init__.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,28 @@
"""Reads vehicle status from MyBMW portal."""

from __future__ import annotations

from dataclasses import dataclass
import logging
from typing import Any

from bimmer_connected.vehicle import MyBMWVehicle
import voluptuous as vol

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE_ID, CONF_ENTITY_ID, CONF_NAME, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import discovery, entity_registry as er
from homeassistant.helpers import (
device_registry as dr,
discovery,
entity_registry as er,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import ATTR_VIN, ATTRIBUTION, CONF_READ_ONLY, DATA_HASS_CONFIG, DOMAIN
from .const import ATTR_VIN, CONF_READ_ONLY, DOMAIN
from .coordinator import BMWDataUpdateCoordinator

_LOGGER = logging.getLogger(__name__)


CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)

SERVICE_SCHEMA = vol.Schema(
vol.Any(
{vol.Required(ATTR_VIN): cv.string},
Expand All @@ -50,13 +49,14 @@
SERVICE_UPDATE_STATE = "update_state"


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the BMW Connected Drive component from configuration.yaml."""
# Store full yaml config in data for platform.NOTIFY
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][DATA_HASS_CONFIG] = config
type BMWConfigEntry = ConfigEntry[BMWData]

return True

@dataclass
class BMWData:
"""Class to store BMW runtime data."""

coordinator: BMWDataUpdateCoordinator


@callback
Expand All @@ -67,23 +67,38 @@ def _async_migrate_options_from_data_if_missing(
options = dict(entry.options)

if CONF_READ_ONLY in data or list(options) != list(DEFAULT_OPTIONS):
options = dict(DEFAULT_OPTIONS, **options)
options = dict(
DEFAULT_OPTIONS,
**{k: v for k, v in options.items() if k in DEFAULT_OPTIONS},
)
options[CONF_READ_ONLY] = data.pop(CONF_READ_ONLY, False)

hass.config_entries.async_update_entry(entry, data=data, options=options)


async def _async_migrate_entries(
hass: HomeAssistant, config_entry: ConfigEntry
hass: HomeAssistant, config_entry: BMWConfigEntry
) -> bool:
"""Migrate old entry."""
entity_registry = er.async_get(hass)

@callback
def update_unique_id(entry: er.RegistryEntry) -> dict[str, str] | None:
replacements = {
"charging_level_hv": "remaining_battery_percent",
"fuel_percent": "remaining_fuel_percent",
"charging_level_hv": "fuel_and_battery.remaining_battery_percent",
"fuel_percent": "fuel_and_battery.remaining_fuel_percent",
"ac_current_limit": "charging_profile.ac_current_limit",
"charging_start_time": "fuel_and_battery.charging_start_time",
"charging_end_time": "fuel_and_battery.charging_end_time",
"charging_status": "fuel_and_battery.charging_status",
"charging_target": "fuel_and_battery.charging_target",
"remaining_battery_percent": "fuel_and_battery.remaining_battery_percent",
"remaining_range_total": "fuel_and_battery.remaining_range_total",
"remaining_range_electric": "fuel_and_battery.remaining_range_electric",
"remaining_range_fuel": "fuel_and_battery.remaining_range_fuel",
"remaining_fuel": "fuel_and_battery.remaining_fuel",
"remaining_fuel_percent": "fuel_and_battery.remaining_fuel_percent",
"activity": "climate.activity",
}
if (key := entry.unique_id.split("-")[-1]) in replacements:
new_unique_id = entry.unique_id.replace(key, replacements[key])
Expand Down Expand Up @@ -126,8 +141,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
await coordinator.async_config_entry_first_refresh()

hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator
entry.runtime_data = BMWData(coordinator)

# Set up all platforms except notify
await hass.config_entries.async_forward_entry_setups(
Expand All @@ -142,54 +156,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
Platform.NOTIFY,
DOMAIN,
{CONF_NAME: DOMAIN, CONF_ENTITY_ID: entry.entry_id},
hass.data[DOMAIN][DATA_HASS_CONFIG],
{},
)
)

# Clean up vehicles which are not assigned to the account anymore
account_vehicles = {(DOMAIN, v.vin) for v in coordinator.account.vehicles}
device_registry = dr.async_get(hass)
device_entries = dr.async_entries_for_config_entry(
device_registry, config_entry_id=entry.entry_id
)
for device in device_entries:
if not device.identifiers.intersection(account_vehicles):
device_registry.async_update_device(
device.id, remove_config_entry_id=entry.entry_id
)

return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(

return await hass.config_entries.async_unload_platforms(
entry, [platform for platform in PLATFORMS if platform != Platform.NOTIFY]
)

if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)

return unload_ok


class BMWBaseEntity(CoordinatorEntity[BMWDataUpdateCoordinator]):
"""Common base for BMW entities."""

coordinator: BMWDataUpdateCoordinator
_attr_attribution = ATTRIBUTION
_attr_has_entity_name = True

def __init__(
self,
coordinator: BMWDataUpdateCoordinator,
vehicle: MyBMWVehicle,
) -> None:
"""Initialize entity."""
super().__init__(coordinator)

self.vehicle = vehicle

self._attrs: dict[str, Any] = {
"car": self.vehicle.name,
"vin": self.vehicle.vin,
}
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self.vehicle.vin)},
manufacturer=vehicle.brand.name,
model=vehicle.name,
name=vehicle.name,
)

async def async_added_to_hass(self) -> None:
"""When entity is added to hass."""
await super().async_added_to_hass()
self._handle_coordinator_update()
68 changes: 31 additions & 37 deletions custom_components/bmw_connected_drive/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Reads vehicle status from BMW MyBMW portal."""

from __future__ import annotations

from collections.abc import Callable
Expand All @@ -16,20 +17,22 @@
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.unit_system import UnitSystem

from . import BMWBaseEntity
from .const import DOMAIN, UNIT_MAP
from . import BMWConfigEntry
from .const import UNIT_MAP
from .coordinator import BMWDataUpdateCoordinator
from .entity import BMWBaseEntity

_LOGGER = logging.getLogger(__name__)


ALLOWED_CONDITION_BASED_SERVICE_KEYS = {
"BRAKE_FLUID",
"BRAKE_PADS_FRONT",
"BRAKE_PADS_REAR",
"EMISSION_CHECK",
"ENGINE_OIL",
"OIL",
Expand All @@ -40,7 +43,11 @@
}
LOGGED_CONDITION_BASED_SERVICE_WARNINGS: set[str] = set()

ALLOWED_CHECK_CONTROL_MESSAGE_KEYS = {"ENGINE_OIL", "TIRE_PRESSURE"}
ALLOWED_CHECK_CONTROL_MESSAGE_KEYS = {
"ENGINE_OIL",
"TIRE_PRESSURE",
"WASHING_FLUID",
}
LOGGED_CHECK_CONTROL_MESSAGE_WARNINGS: set[str] = set()


Expand Down Expand Up @@ -103,28 +110,20 @@ def _format_cbs_report(
return result


@dataclass
class BMWRequiredKeysMixin:
"""Mixin for required keys."""

value_fn: Callable[[MyBMWVehicle], bool]


@dataclass
class BMWBinarySensorEntityDescription(
BinarySensorEntityDescription, BMWRequiredKeysMixin
):
@dataclass(frozen=True, kw_only=True)
class BMWBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Describes BMW binary_sensor entity."""

value_fn: Callable[[MyBMWVehicle], bool]
attr_fn: Callable[[MyBMWVehicle, UnitSystem], dict[str, Any]] | None = None
is_available: Callable[[MyBMWVehicle], bool] = lambda v: v.is_lsc_enabled


SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = (
BMWBinarySensorEntityDescription(
key="lids",
name="Lids",
translation_key="lids",
device_class=BinarySensorDeviceClass.OPENING,
icon="mdi:car-door-lock",
# device class opening: On means open, Off means closed
value_fn=lambda v: not v.doors_and_windows.all_lids_closed,
attr_fn=lambda v, u: {
Expand All @@ -133,9 +132,8 @@ class BMWBinarySensorEntityDescription(
),
BMWBinarySensorEntityDescription(
key="windows",
name="Windows",
translation_key="windows",
device_class=BinarySensorDeviceClass.OPENING,
icon="mdi:car-door",
# device class opening: On means open, Off means closed
value_fn=lambda v: not v.doors_and_windows.all_windows_closed,
attr_fn=lambda v, u: {
Expand All @@ -144,9 +142,8 @@ class BMWBinarySensorEntityDescription(
),
BMWBinarySensorEntityDescription(
key="door_lock_state",
name="Door lock state",
translation_key="door_lock_state",
device_class=BinarySensorDeviceClass.LOCK,
icon="mdi:car-key",
# device class lock: On means unlocked, Off means locked
# Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED
value_fn=lambda v: v.doors_and_windows.door_lock_state
Expand All @@ -157,62 +154,60 @@ class BMWBinarySensorEntityDescription(
),
BMWBinarySensorEntityDescription(
key="condition_based_services",
name="Condition based services",
translation_key="condition_based_services",
device_class=BinarySensorDeviceClass.PROBLEM,
icon="mdi:wrench",
# device class problem: On means problem detected, Off means no problem
value_fn=lambda v: v.condition_based_services.is_service_required,
attr_fn=_condition_based_services,
),
BMWBinarySensorEntityDescription(
key="check_control_messages",
name="Check control messages",
translation_key="check_control_messages",
device_class=BinarySensorDeviceClass.PROBLEM,
icon="mdi:car-tire-alert",
# device class problem: On means problem detected, Off means no problem
value_fn=lambda v: v.check_control_messages.has_check_control_messages,
attr_fn=lambda v, u: _check_control_messages(v),
),
# electric
BMWBinarySensorEntityDescription(
key="charging_status",
name="Charging status",
translation_key="charging_status",
device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
icon="mdi:ev-station",
# device class power: On means power detected, Off means no power
value_fn=lambda v: v.fuel_and_battery.charging_status == ChargingState.CHARGING,
is_available=lambda v: v.has_electric_drivetrain,
),
BMWBinarySensorEntityDescription(
key="connection_status",
name="Connection status",
translation_key="connection_status",
device_class=BinarySensorDeviceClass.PLUG,
icon="mdi:car-electric",
value_fn=lambda v: v.fuel_and_battery.is_charger_connected,
is_available=lambda v: v.has_electric_drivetrain,
),
BMWBinarySensorEntityDescription(
key="is_pre_entry_climatization_enabled",
name="Pre entry climatization",
icon="mdi:car-seat-heater",
translation_key="is_pre_entry_climatization_enabled",
value_fn=lambda v: v.charging_profile.is_pre_entry_climatization_enabled
if v.charging_profile
else False,
is_available=lambda v: v.has_electric_drivetrain,
),
)


async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: BMWConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the BMW binary sensors from config entry."""
coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
coordinator = config_entry.runtime_data.coordinator

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

Expand Down Expand Up @@ -246,9 +241,8 @@ def _handle_coordinator_update(self) -> None:
self._attr_is_on = self.entity_description.value_fn(self.vehicle)

if self.entity_description.attr_fn:
self._attr_extra_state_attributes = dict(
self._attrs,
**self.entity_description.attr_fn(self.vehicle, self._unit_system),
self._attr_extra_state_attributes = self.entity_description.attr_fn(
self.vehicle, self._unit_system
)

super()._handle_coordinator_update()
Loading

0 comments on commit 4a3e6e8

Please sign in to comment.