diff --git a/custom_components/bmw_connected_drive/__init__.py b/custom_components/bmw_connected_drive/__init__.py index e681cac..400ab50 100644 --- a/custom_components/bmw_connected_drive/__init__.py +++ b/custom_components/bmw_connected_drive/__init__.py @@ -68,6 +68,7 @@ PLATFORMS = [ Platform.BINARY_SENSOR, + Platform.BUTTON, Platform.DEVICE_TRACKER, Platform.LOCK, Platform.NOTIFY, @@ -163,7 +164,7 @@ def _update_all() -> None: hass.async_create_task( discovery.async_load_platform( hass, - NOTIFY_DOMAIN, + Platform.NOTIFY, DOMAIN, {CONF_NAME: DOMAIN}, hass.data[DOMAIN][DATA_HASS_CONFIG], @@ -223,6 +224,11 @@ def setup_account( def execute_service(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) diff --git a/custom_components/bmw_connected_drive/binary_sensor.py b/custom_components/bmw_connected_drive/binary_sensor.py index 37c0271..8110b53 100644 --- a/custom_components/bmw_connected_drive/binary_sensor.py +++ b/custom_components/bmw_connected_drive/binary_sensor.py @@ -15,12 +15,7 @@ ) from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_BATTERY_CHARGING, - DEVICE_CLASS_LIGHT, - DEVICE_CLASS_LOCK, - DEVICE_CLASS_OPENING, - DEVICE_CLASS_PLUG, - DEVICE_CLASS_PROBLEM, + BinarySensorDeviceClass, BinarySensorEntity, BinarySensorEntityDescription, ) @@ -158,42 +153,42 @@ class BMWBinarySensorEntityDescription( BMWBinarySensorEntityDescription( key="lids", name="Doors", - device_class=DEVICE_CLASS_OPENING, + device_class=BinarySensorDeviceClass.OPENING, icon="mdi:car-door-lock", value_fn=_are_doors_closed, ), BMWBinarySensorEntityDescription( key="windows", name="Windows", - device_class=DEVICE_CLASS_OPENING, + device_class=BinarySensorDeviceClass.OPENING, icon="mdi:car-door", value_fn=_are_windows_closed, ), BMWBinarySensorEntityDescription( key="door_lock_state", name="Door lock state", - device_class=DEVICE_CLASS_LOCK, + device_class=BinarySensorDeviceClass.LOCK, icon="mdi:car-key", value_fn=_are_doors_locked, ), BMWBinarySensorEntityDescription( key="lights_parking", name="Parking lights", - device_class=DEVICE_CLASS_LIGHT, + device_class=BinarySensorDeviceClass.LIGHT, icon="mdi:car-parking-lights", value_fn=_are_parking_lights_on, ), BMWBinarySensorEntityDescription( key="condition_based_services", name="Condition based services", - device_class=DEVICE_CLASS_PROBLEM, + device_class=BinarySensorDeviceClass.PROBLEM, icon="mdi:wrench", value_fn=_are_problems_detected, ), BMWBinarySensorEntityDescription( key="check_control_messages", name="Control messages", - device_class=DEVICE_CLASS_PROBLEM, + device_class=BinarySensorDeviceClass.PROBLEM, icon="mdi:car-tire-alert", value_fn=_check_control_messages, ), @@ -201,14 +196,14 @@ class BMWBinarySensorEntityDescription( BMWBinarySensorEntityDescription( key="charging_status", name="Charging status", - device_class=DEVICE_CLASS_BATTERY_CHARGING, + device_class=BinarySensorDeviceClass.BATTERY_CHARGING, icon="mdi:ev-station", value_fn=_is_vehicle_charging, ), BMWBinarySensorEntityDescription( key="connection_status", name="Connection status", - device_class=DEVICE_CLASS_PLUG, + device_class=BinarySensorDeviceClass.PLUG, icon="mdi:car-electric", value_fn=_is_vehicle_plugged_in, ), diff --git a/custom_components/bmw_connected_drive/button.py b/custom_components/bmw_connected_drive/button.py new file mode 100644 index 0000000..72d66d7 --- /dev/null +++ b/custom_components/bmw_connected_drive/button.py @@ -0,0 +1,122 @@ +"""Support for BMW connected drive button entities.""" +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass + +from bimmer_connected.remote_services import RemoteServiceStatus +from bimmer_connected.vehicle import ConnectedDriveVehicle + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.config_entries import ConfigEntry +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 + + +@dataclass +class BMWButtonEntityDescription(ButtonEntityDescription): + """Class describing BMW button entities.""" + + enabled_when_read_only: bool = False + remote_function: Callable[ + [ConnectedDriveVehicle], RemoteServiceStatus + ] | None = None + account_function: Callable[[BMWConnectedDriveAccount], None] | None = None + + +BUTTON_TYPES: tuple[BMWButtonEntityDescription, ...] = ( + BMWButtonEntityDescription( + key="light_flash", + icon="mdi:car-light-alert", + name="Flash Lights", + remote_function=lambda vehicle: vehicle.remote_services.trigger_remote_light_flash(), + ), + BMWButtonEntityDescription( + key="sound_horn", + icon="mdi:bullhorn", + name="Sound Horn", + remote_function=lambda vehicle: vehicle.remote_services.trigger_remote_horn(), + ), + BMWButtonEntityDescription( + key="activate_air_conditioning", + icon="mdi:hvac", + 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", + name="Find Vehicle", + remote_function=lambda vehicle: vehicle.remote_services.trigger_remote_vehicle_finder(), + ), + BMWButtonEntityDescription( + key="refresh", + icon="mdi:refresh", + name="Refresh from cloud", + account_function=lambda account: account.update(), + enabled_when_read_only=True, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + 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] + entities: list[BMWButton] = [] + + for vehicle in account.account.vehicles: + entities.extend( + [ + BMWButton(account, vehicle, description) + for description in BUTTON_TYPES + if not account.read_only + or (account.read_only and description.enabled_when_read_only) + ] + ) + + async_add_entities(entities) + + +class BMWButton(BMWConnectedDriveBaseEntity, ButtonEntity): + """Representation of a BMW Connected Drive button.""" + + entity_description: BMWButtonEntityDescription + + def __init__( + self, + account: BMWConnectedDriveAccount, + vehicle: ConnectedDriveVehicle, + description: BMWButtonEntityDescription, + ) -> None: + """Initialize BMW vehicle sensor.""" + super().__init__(account, 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.""" + if self.entity_description.remote_function: + self.entity_description.remote_function(self._vehicle) + elif self.entity_description.account_function: + self.entity_description.account_function(self._account) diff --git a/custom_components/bmw_connected_drive/const.py b/custom_components/bmw_connected_drive/const.py index 83609d2..0f79a16 100644 --- a/custom_components/bmw_connected_drive/const.py +++ b/custom_components/bmw_connected_drive/const.py @@ -8,6 +8,8 @@ ATTRIBUTION = "Data provided by BMW Connected Drive" +ATTR_DIRECTION = "direction" + CONF_ALLOWED_REGIONS = ["china", "north_america", "rest_of_world"] CONF_READ_ONLY = "read_only" CONF_USE_LOCATION = "use_location" diff --git a/custom_components/bmw_connected_drive/device_tracker.py b/custom_components/bmw_connected_drive/device_tracker.py index 0ba2d50..9062921 100644 --- a/custom_components/bmw_connected_drive/device_tracker.py +++ b/custom_components/bmw_connected_drive/device_tracker.py @@ -17,7 +17,7 @@ BMWConnectedDriveAccount, BMWConnectedDriveBaseEntity, ) -from .const import CONF_ACCOUNT, DATA_ENTRIES +from .const import ATTR_DIRECTION, CONF_ACCOUNT, DATA_ENTRIES _LOGGER = logging.getLogger(__name__) @@ -78,9 +78,11 @@ def source_type(self) -> Literal["gps"]: return SOURCE_TYPE_GPS def update(self) -> None: - """Update state of the decvice tracker.""" + """Update state of the device tracker.""" _LOGGER.debug("Updating device tracker of %s", self._vehicle.name) - self._attr_extra_state_attributes = self._attrs + 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 diff --git a/custom_components/bmw_connected_drive/manifest.json b/custom_components/bmw_connected_drive/manifest.json index 7b43528..16194ee 100644 --- a/custom_components/bmw_connected_drive/manifest.json +++ b/custom_components/bmw_connected_drive/manifest.json @@ -5,6 +5,6 @@ "requirements": ["bimmer_connected==0.8.10"], "codeowners": ["@gerard33", "@rikroe"], "config_flow": true, - "version": "2021.12.9-custom", + "version": "2022.2.9-custom", "iot_class": "cloud_polling" } diff --git a/custom_components/bmw_connected_drive/sensor.py b/custom_components/bmw_connected_drive/sensor.py index 61792f3..f21c1b8 100644 --- a/custom_components/bmw_connected_drive/sensor.py +++ b/custom_components/bmw_connected_drive/sensor.py @@ -8,12 +8,14 @@ from bimmer_connected.vehicle import ConnectedDriveVehicle -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_UNIT_SYSTEM_IMPERIAL, - DEVICE_CLASS_BATTERY, - DEVICE_CLASS_TIMESTAMP, LENGTH_KILOMETERS, LENGTH_MILES, PERCENTAGE, @@ -48,12 +50,12 @@ class BMWSensorEntityDescription(SensorEntityDescription): # --- Generic --- "charging_start_time": BMWSensorEntityDescription( key="charging_start_time", - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, entity_registry_enabled_default=False, ), "charging_end_time": BMWSensorEntityDescription( key="charging_end_time", - device_class=DEVICE_CLASS_TIMESTAMP, + device_class=SensorDeviceClass.TIMESTAMP, ), "charging_time_label": BMWSensorEntityDescription( key="charging_time_label", @@ -68,7 +70,7 @@ class BMWSensorEntityDescription(SensorEntityDescription): key="charging_level_hv", unit_metric=PERCENTAGE, unit_imperial=PERCENTAGE, - device_class=DEVICE_CLASS_BATTERY, + device_class=SensorDeviceClass.BATTERY, ), # --- Specific --- "mileage": BMWSensorEntityDescription( @@ -77,7 +79,7 @@ class BMWSensorEntityDescription(SensorEntityDescription): unit_metric=LENGTH_KILOMETERS, unit_imperial=LENGTH_MILES, value=lambda x, hass: round( - hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])) + hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])), 2 ), ), "remaining_range_total": BMWSensorEntityDescription( @@ -86,7 +88,7 @@ class BMWSensorEntityDescription(SensorEntityDescription): unit_metric=LENGTH_KILOMETERS, unit_imperial=LENGTH_MILES, value=lambda x, hass: round( - hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])) + hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])), 2 ), ), "remaining_range_electric": BMWSensorEntityDescription( @@ -95,7 +97,7 @@ class BMWSensorEntityDescription(SensorEntityDescription): unit_metric=LENGTH_KILOMETERS, unit_imperial=LENGTH_MILES, value=lambda x, hass: round( - hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])) + hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])), 2 ), ), "remaining_range_fuel": BMWSensorEntityDescription( @@ -104,7 +106,7 @@ class BMWSensorEntityDescription(SensorEntityDescription): unit_metric=LENGTH_KILOMETERS, unit_imperial=LENGTH_MILES, value=lambda x, hass: round( - hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])) + hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])), 2 ), ), "remaining_fuel": BMWSensorEntityDescription( @@ -113,7 +115,7 @@ class BMWSensorEntityDescription(SensorEntityDescription): unit_metric=VOLUME_LITERS, unit_imperial=VOLUME_GALLONS, value=lambda x, hass: round( - hass.config.units.volume(x[0], UNIT_MAP.get(x[1], x[1])) + hass.config.units.volume(x[0], UNIT_MAP.get(x[1], x[1])), 2 ), ), "fuel_percent": BMWSensorEntityDescription( diff --git a/custom_components/bmw_connected_drive/translations/it.json b/custom_components/bmw_connected_drive/translations/it.json index 277ed18..eeca190 100644 --- a/custom_components/bmw_connected_drive/translations/it.json +++ b/custom_components/bmw_connected_drive/translations/it.json @@ -22,7 +22,7 @@ "account_options": { "data": { "read_only": "Sola lettura (solo sensori e notifica, nessuna esecuzione di servizi, nessun blocco)", - "use_location": "Usa la posizione di Home Assistant per richieste sulla posizione dell'auto (richiesto per veicoli non i3/i8 prodotti prima del 7/2014)" + "use_location": "Usa la posizione di Home Assistant per le richieste di posizione dell'auto (richiesto per veicoli non i3/i8 prodotti prima del 7/2014)" } } } diff --git a/custom_components/bmw_connected_drive/translations/ja.json b/custom_components/bmw_connected_drive/translations/ja.json index 5e33b26..a3fa863 100644 --- a/custom_components/bmw_connected_drive/translations/ja.json +++ b/custom_components/bmw_connected_drive/translations/ja.json @@ -22,7 +22,7 @@ "account_options": { "data": { "read_only": "\u30ea\u30fc\u30c9\u30aa\u30f3\u30ea\u30fc(\u30bb\u30f3\u30b5\u30fc\u3068\u901a\u77e5\u306e\u307f\u3001\u30b5\u30fc\u30d3\u30b9\u306e\u5b9f\u884c\u306f\u4e0d\u53ef\u3001\u30ed\u30c3\u30af\u4e0d\u53ef)", - "use_location": "Home Assistant\u306e\u5834\u6240\u3092\u3001\u8eca\u306e\u4f4d\u7f6e\u3068\u3057\u3066\u30dd\u30fc\u30ea\u30f3\u30b0\u306b\u4f7f\u7528\u3059\u308b((2014\u5e747\u67087\u65e5\u4ee5\u524d\u306b\u751f\u7523\u3055\u308c\u305f\u3001i3/i8\u4ee5\u5916\u306e\u8eca\u4e21\u3067\u306f\u5fc5\u9808)" + "use_location": "Home Assistant\u306e\u5834\u6240\u3092\u3001\u8eca\u306e\u4f4d\u7f6e\u3068\u3057\u3066\u30dd\u30fc\u30ea\u30f3\u30b0\u306b\u4f7f\u7528\u3059\u308b(2014\u5e747\u67087\u65e5\u4ee5\u524d\u306b\u751f\u7523\u3055\u308c\u305f\u3001i3/i8\u4ee5\u5916\u306e\u8eca\u4e21\u3067\u306f\u5fc5\u9808)" } } }