diff --git a/custom_components/bmw_connected_drive/__init__.py b/custom_components/bmw_connected_drive/__init__.py index 8d5d842..27f2d99 100644 --- a/custom_components/bmw_connected_drive/__init__.py +++ b/custom_components/bmw_connected_drive/__init__.py @@ -44,6 +44,7 @@ Platform.NUMBER, Platform.SELECT, Platform.SENSOR, + Platform.SWITCH, ] SERVICE_UPDATE_STATE = "update_state" diff --git a/custom_components/bmw_connected_drive/button.py b/custom_components/bmw_connected_drive/button.py index 873a727..5285820 100644 --- a/custom_components/bmw_connected_drive/button.py +++ b/custom_components/bmw_connected_drive/button.py @@ -53,12 +53,6 @@ class BMWButtonEntityDescription(ButtonEntityDescription): name="Activate air conditioning", remote_function=lambda vehicle: vehicle.remote_services.trigger_remote_air_conditioning(), ), - BMWButtonEntityDescription( - key="deactivate_air_conditioning", - icon="mdi:hvac-off", - name="Deactivate air conditioning", - remote_function=lambda vehicle: vehicle.remote_services.trigger_remote_air_conditioning_stop(), - ), BMWButtonEntityDescription( key="find_vehicle", icon="mdi:crosshairs-question", @@ -128,7 +122,4 @@ async def async_press(self) -> None: ) 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.async_update_listeners() diff --git a/custom_components/bmw_connected_drive/lock.py b/custom_components/bmw_connected_drive/lock.py index ffc6cf6..d20ccd1 100644 --- a/custom_components/bmw_connected_drive/lock.py +++ b/custom_components/bmw_connected_drive/lock.py @@ -68,6 +68,8 @@ async def async_lock(self, **kwargs: Any) -> None: self.async_write_ha_state() await self.vehicle.remote_services.trigger_remote_door_lock() + self.coordinator.async_update_listeners() + async def async_unlock(self, **kwargs: Any) -> None: """Unlock the car.""" _LOGGER.debug("%s: unlocking doors", self.vehicle.name) @@ -79,6 +81,8 @@ async def async_unlock(self, **kwargs: Any) -> None: self.async_write_ha_state() await self.vehicle.remote_services.trigger_remote_door_unlock() + self.coordinator.async_update_listeners() + @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" diff --git a/custom_components/bmw_connected_drive/manifest.json b/custom_components/bmw_connected_drive/manifest.json index c600a15..8c9cc91 100644 --- a/custom_components/bmw_connected_drive/manifest.json +++ b/custom_components/bmw_connected_drive/manifest.json @@ -6,5 +6,6 @@ "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", "iot_class": "cloud_polling", "loggers": ["bimmer_connected"], - "requirements": ["bimmer_connected==0.13.5"] + "version": "2023.6.0b4", + "requirements": ["bimmer_connected==0.13.6"] } diff --git a/custom_components/bmw_connected_drive/number.py b/custom_components/bmw_connected_drive/number.py index 820257f..c8f72b2 100644 --- a/custom_components/bmw_connected_drive/number.py +++ b/custom_components/bmw_connected_drive/number.py @@ -116,3 +116,5 @@ async def async_set_native_value(self, value: float) -> None: await self.entity_description.remote_service(self.vehicle, value) except MyBMWAPIError as ex: raise HomeAssistantError(ex) from ex + + self.coordinator.async_update_listeners() diff --git a/custom_components/bmw_connected_drive/select.py b/custom_components/bmw_connected_drive/select.py index 52d35b4..0b20ed9 100644 --- a/custom_components/bmw_connected_drive/select.py +++ b/custom_components/bmw_connected_drive/select.py @@ -124,3 +124,5 @@ async def async_select_option(self, option: str) -> None: option, ) await self.entity_description.remote_service(self.vehicle, option) + + self.coordinator.async_update_listeners() diff --git a/custom_components/bmw_connected_drive/sensor.py b/custom_components/bmw_connected_drive/sensor.py index bf67f77..314ff47 100644 --- a/custom_components/bmw_connected_drive/sensor.py +++ b/custom_components/bmw_connected_drive/sensor.py @@ -95,13 +95,6 @@ def convert_and_round( unit_type=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, ), - "charging_mode": BMWSensorEntityDescription( - key="charging_mode", - name="Charging mode", - key_class="charging_profile", - icon="mdi:ev-station", - value=lambda x, y: x.value, - ), # --- Specific --- "mileage": BMWSensorEntityDescription( key="mileage", diff --git a/custom_components/bmw_connected_drive/switch.py b/custom_components/bmw_connected_drive/switch.py new file mode 100644 index 0000000..41243ca --- /dev/null +++ b/custom_components/bmw_connected_drive/switch.py @@ -0,0 +1,132 @@ +"""Switch platform for BMW.""" + +from collections.abc import Callable, Coroutine +from dataclasses import dataclass +import logging +from typing import Any + +from bimmer_connected.models import MyBMWAPIError +from bimmer_connected.vehicle import MyBMWVehicle +from bimmer_connected.vehicle.fuel_and_battery import ChargingState + +from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import BMWBaseEntity +from .const import DOMAIN +from .coordinator import BMWDataUpdateCoordinator + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class BMWRequiredKeysMixin: + """Mixin for required keys.""" + + value_fn: Callable[[MyBMWVehicle], bool] + remote_service_on: Callable[[MyBMWVehicle], Coroutine[Any, Any, Any]] + remote_service_off: Callable[[MyBMWVehicle], Coroutine[Any, Any, Any]] + + +@dataclass +class BMWSwitchEntityDescription(SwitchEntityDescription, BMWRequiredKeysMixin): + """Describes BMW switch entity.""" + + is_available: Callable[[MyBMWVehicle], bool] = lambda _: False + dynamic_options: Callable[[MyBMWVehicle], list[str]] | None = None + + +CHARGING_STATE_ON = { + ChargingState.CHARGING, + ChargingState.COMPLETE, + ChargingState.FULLY_CHARGED, + ChargingState.FINISHED_FULLY_CHARGED, + ChargingState.FINISHED_NOT_FULL, + ChargingState.TARGET_REACHED, +} + +NUMBER_TYPES: list[BMWSwitchEntityDescription] = [ + BMWSwitchEntityDescription( + key="climate", + name="Climate", + is_available=lambda v: v.is_remote_climate_stop_enabled, + value_fn=lambda v: v.climate.is_climate_on, + remote_service_on=lambda v: v.remote_services.trigger_remote_air_conditioning(), + remote_service_off=lambda v: v.remote_services.trigger_remote_air_conditioning_stop(), + icon="mdi:fan", + ), + BMWSwitchEntityDescription( + key="charging", + name="Charging", + is_available=lambda v: v.is_remote_charge_stop_enabled, + value_fn=lambda v: v.fuel_and_battery.charging_status in CHARGING_STATE_ON, + remote_service_on=lambda v: v.remote_services.trigger_charge_start(), + remote_service_off=lambda v: v.remote_services.trigger_charge_stop(), + icon="mdi:ev-station", + ), +] + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the MyBMW switch from config entry.""" + coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + entities: list[BMWSwitch] = [] + + for vehicle in coordinator.account.vehicles: + if not coordinator.read_only: + entities.extend( + [ + BMWSwitch(coordinator, vehicle, description) + for description in NUMBER_TYPES + if description.is_available(vehicle) + ] + ) + async_add_entities(entities) + + +class BMWSwitch(BMWBaseEntity, SwitchEntity): + """Representation of BMW Switch entity.""" + + entity_description: BMWSwitchEntityDescription + + def __init__( + self, + coordinator: BMWDataUpdateCoordinator, + vehicle: MyBMWVehicle, + description: BMWSwitchEntityDescription, + ) -> None: + """Initialize an BMW Switch.""" + super().__init__(coordinator, vehicle) + self.entity_description = description + self._attr_unique_id = f"{vehicle.vin}-{description.key}" + + @property + def is_on(self) -> bool: + """Return the entity value to represent the entity state.""" + return self.entity_description.value_fn(self.vehicle) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the switch on.""" + try: + await self.entity_description.remote_service_on(self.vehicle) + except MyBMWAPIError as ex: + raise HomeAssistantError(ex) from ex + + self.coordinator.async_update_listeners() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the switch off.""" + try: + await self.entity_description.remote_service_off(self.vehicle) + except MyBMWAPIError as ex: + raise HomeAssistantError(ex) from ex + + self.coordinator.async_update_listeners()