Skip to content

Commit

Permalink
Use value_fn for bmw_connected_drive binary_sensor (#57540)
Browse files Browse the repository at this point in the history
  • Loading branch information
cdce8p committed Oct 22, 2021
1 parent 1aa7a81 commit fa56be7
Showing 1 changed file with 166 additions and 82 deletions.
248 changes: 166 additions & 82 deletions homeassistant/components/bmw_connected_drive/binary_sensor.py
@@ -1,9 +1,14 @@
"""Reads vehicle status from BMW connected drive portal."""
from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass
import logging
from typing import Any

from bimmer_connected.state import ChargingState, LockState
from bimmer_connected.vehicle import ConnectedDriveVehicle
from bimmer_connected.vehicle_status import ConditionBasedServiceReport, VehicleStatus

from homeassistant.components.binary_sensor import (
DEVICE_CLASS_OPENING,
Expand All @@ -12,72 +17,205 @@
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import LENGTH_KILOMETERS

from . import DOMAIN as BMW_DOMAIN, BMWConnectedDriveBaseEntity
from homeassistant.core import HomeAssistant
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

_LOGGER = logging.getLogger(__name__)

SENSOR_TYPES: tuple[BinarySensorEntityDescription, ...] = (
BinarySensorEntityDescription(

def _are_doors_closed(
vehicle_state: VehicleStatus, extra_attributes: dict[str, Any], *args: Any
) -> bool:
# device class opening: On means open, Off means closed
_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 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:
extra_attributes.update(_format_cbs_report(report, unit_system))
return not vehicle_state.are_all_cbs_ok


def _check_control_messages(
vehicle_state: VehicleStatus, extra_attributes: dict[str, Any], *args: Any
) -> bool:
# device class problem: On means problem detected, Off means no problem
check_control_messages = vehicle_state.check_control_messages
has_check_control_messages = vehicle_state.has_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
else:
extra_attributes["check_control_messages"] = "OK"
return vehicle_state.has_check_control_messages


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 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 vehicle_state.connection_status == "CONNECTED"


def _format_cbs_report(
report: ConditionBasedServiceReport, unit_system: UnitSystem
) -> dict[str, Any]:
result: dict[str, Any] = {}
service_type = report.service_type.lower().replace("_", " ")
result[f"{service_type} status"] = report.state.value
if report.due_date is not None:
result[f"{service_type} date"] = report.due_date.strftime("%Y-%m-%d")
if report.due_distance is not None:
distance = round(unit_system.length(report.due_distance, LENGTH_KILOMETERS))
result[f"{service_type} distance"] = f"{distance} {unit_system.length_unit}"
return result


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

value_fn: Callable[[VehicleStatus, dict[str, Any], UnitSystem], bool]


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


SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = (
BMWBinarySensorEntityDescription(
key="lids",
name="Doors",
device_class=DEVICE_CLASS_OPENING,
icon="mdi:car-door-lock",
value_fn=_are_doors_closed,
),
BinarySensorEntityDescription(
BMWBinarySensorEntityDescription(
key="windows",
name="Windows",
device_class=DEVICE_CLASS_OPENING,
icon="mdi:car-door",
value_fn=_are_windows_closed,
),
BinarySensorEntityDescription(
BMWBinarySensorEntityDescription(
key="door_lock_state",
name="Door lock state",
device_class="lock",
icon="mdi:car-key",
value_fn=_are_doors_locked,
),
BinarySensorEntityDescription(
BMWBinarySensorEntityDescription(
key="lights_parking",
name="Parking lights",
device_class="light",
icon="mdi:car-parking-lights",
value_fn=_are_parking_lights_on,
),
BinarySensorEntityDescription(
BMWBinarySensorEntityDescription(
key="condition_based_services",
name="Condition based services",
device_class=DEVICE_CLASS_PROBLEM,
icon="mdi:wrench",
value_fn=_are_problems_detected,
),
BinarySensorEntityDescription(
BMWBinarySensorEntityDescription(
key="check_control_messages",
name="Control messages",
device_class=DEVICE_CLASS_PROBLEM,
icon="mdi:car-tire-alert",
value_fn=_check_control_messages,
),
# electric
BinarySensorEntityDescription(
BMWBinarySensorEntityDescription(
key="charging_status",
name="Charging status",
device_class="power",
icon="mdi:ev-station",
value_fn=_is_vehicle_charging,
),
BinarySensorEntityDescription(
BMWBinarySensorEntityDescription(
key="connection_status",
name="Connection status",
device_class=DEVICE_CLASS_PLUG,
icon="mdi:car-electric",
value_fn=_is_vehicle_plugged_in,
),
)


async def async_setup_entry(hass, config_entry, async_add_entities):
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the BMW ConnectedDrive binary sensors from config entry."""
account = hass.data[BMW_DOMAIN][DATA_ENTRIES][config_entry.entry_id][CONF_ACCOUNT]
account: BMWConnectedDriveAccount = hass.data[BMW_DOMAIN][DATA_ENTRIES][
config_entry.entry_id
][CONF_ACCOUNT]

entities = [
BMWConnectedDriveSensor(account, vehicle, description)
BMWConnectedDriveSensor(account, vehicle, description, hass.config.units)
for vehicle in account.account.vehicles
for description in SENSOR_TYPES
if description.key in vehicle.available_attributes
Expand All @@ -88,83 +226,29 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
class BMWConnectedDriveSensor(BMWConnectedDriveBaseEntity, BinarySensorEntity):
"""Representation of a BMW vehicle binary sensor."""

def __init__(self, account, vehicle, description: BinarySensorEntityDescription):
entity_description: BMWBinarySensorEntityDescription

def __init__(
self,
account: BMWConnectedDriveAccount,
vehicle: ConnectedDriveVehicle,
description: BMWBinarySensorEntityDescription,
unit_system: UnitSystem,
) -> None:
"""Initialize sensor."""
super().__init__(account, 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):
def update(self) -> None:
"""Read new state data from the library."""
sensor_type = self.entity_description.key
vehicle_state = self._vehicle.state
result = self._attrs.copy()

# device class opening: On means open, Off means closed
if sensor_type == "lids":
_LOGGER.debug("Status of lid: %s", vehicle_state.all_lids_closed)
self._attr_is_on = not vehicle_state.all_lids_closed
for lid in vehicle_state.lids:
result[lid.name] = lid.state.value
elif sensor_type == "windows":
self._attr_is_on = not vehicle_state.all_windows_closed
for window in vehicle_state.windows:
result[window.name] = window.state.value
# device class lock: On means unlocked, Off means locked
elif sensor_type == "door_lock_state":
# Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED
self._attr_is_on = vehicle_state.door_lock_state not in [
LockState.LOCKED,
LockState.SECURED,
]
result["door_lock_state"] = vehicle_state.door_lock_state.value
result["last_update_reason"] = vehicle_state.last_update_reason
# device class light: On means light detected, Off means no light
elif sensor_type == "lights_parking":
self._attr_is_on = vehicle_state.are_parking_lights_on
result["lights_parking"] = vehicle_state.parking_lights.value
# device class problem: On means problem detected, Off means no problem
elif sensor_type == "condition_based_services":
self._attr_is_on = not vehicle_state.are_all_cbs_ok
for report in vehicle_state.condition_based_services:
result.update(self._format_cbs_report(report))
elif sensor_type == "check_control_messages":
self._attr_is_on = vehicle_state.has_check_control_messages
check_control_messages = vehicle_state.check_control_messages
has_check_control_messages = vehicle_state.has_check_control_messages
if has_check_control_messages:
cbs_list = []
for message in check_control_messages:
cbs_list.append(message.description_short)
result["check_control_messages"] = cbs_list
else:
result["check_control_messages"] = "OK"
# device class power: On means power detected, Off means no power
elif sensor_type == "charging_status":
self._attr_is_on = vehicle_state.charging_status in [ChargingState.CHARGING]
result["charging_status"] = vehicle_state.charging_status.value
result["last_charging_end_result"] = vehicle_state.last_charging_end_result
# device class plug: On means device is plugged in,
# Off means device is unplugged
elif sensor_type == "connection_status":
self._attr_is_on = vehicle_state.connection_status == "CONNECTED"
result["connection_status"] = vehicle_state.connection_status

self._attr_is_on = self.entity_description.value_fn(
vehicle_state, result, self._unit_system
)
self._attr_extra_state_attributes = result

def _format_cbs_report(self, report):
result = {}
service_type = report.service_type.lower().replace("_", " ")
result[f"{service_type} status"] = report.state.value
if report.due_date is not None:
result[f"{service_type} date"] = report.due_date.strftime("%Y-%m-%d")
if report.due_distance is not None:
distance = round(
self.hass.config.units.length(report.due_distance, LENGTH_KILOMETERS)
)
result[
f"{service_type} distance"
] = f"{distance} {self.hass.config.units.length_unit}"
return result

0 comments on commit fa56be7

Please sign in to comment.