Home Assistant Unofficial Reference 2024.12.1
binary_sensor.py
Go to the documentation of this file.
1 """Reads vehicle status from BMW MyBMW portal."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 from dataclasses import dataclass
7 import logging
8 from typing import Any
9 
10 from bimmer_connected.vehicle import MyBMWVehicle
11 from bimmer_connected.vehicle.doors_windows import LockState
12 from bimmer_connected.vehicle.fuel_and_battery import ChargingState
13 from bimmer_connected.vehicle.reports import ConditionBasedService
14 
16  BinarySensorDeviceClass,
17  BinarySensorEntity,
18  BinarySensorEntityDescription,
19 )
20 from homeassistant.core import HomeAssistant, callback
21 from homeassistant.helpers.entity_platform import AddEntitiesCallback
22 from homeassistant.util.unit_system import UnitSystem
23 
24 from . import BMWConfigEntry
25 from .const import UNIT_MAP
26 from .coordinator import BMWDataUpdateCoordinator
27 from .entity import BMWBaseEntity
28 
29 _LOGGER = logging.getLogger(__name__)
30 
31 
32 ALLOWED_CONDITION_BASED_SERVICE_KEYS = {
33  "BRAKE_FLUID",
34  "BRAKE_PADS_FRONT",
35  "BRAKE_PADS_REAR",
36  "EMISSION_CHECK",
37  "ENGINE_OIL",
38  "OIL",
39  "TIRE_WEAR_FRONT",
40  "TIRE_WEAR_REAR",
41  "VEHICLE_CHECK",
42  "VEHICLE_TUV",
43 }
44 LOGGED_CONDITION_BASED_SERVICE_WARNINGS: set[str] = set()
45 
46 ALLOWED_CHECK_CONTROL_MESSAGE_KEYS = {
47  "ENGINE_OIL",
48  "TIRE_PRESSURE",
49  "WASHING_FLUID",
50 }
51 LOGGED_CHECK_CONTROL_MESSAGE_WARNINGS: set[str] = set()
52 
53 
55  vehicle: MyBMWVehicle, unit_system: UnitSystem
56 ) -> dict[str, Any]:
57  extra_attributes = {}
58  for report in vehicle.condition_based_services.messages:
59  if (
60  report.service_type not in ALLOWED_CONDITION_BASED_SERVICE_KEYS
61  and report.service_type not in LOGGED_CONDITION_BASED_SERVICE_WARNINGS
62  ):
63  _LOGGER.warning(
64  "'%s' not an allowed condition based service (%s)",
65  report.service_type,
66  report,
67  )
68  LOGGED_CONDITION_BASED_SERVICE_WARNINGS.add(report.service_type)
69  continue
70 
71  extra_attributes.update(_format_cbs_report(report, unit_system))
72  return extra_attributes
73 
74 
75 def _check_control_messages(vehicle: MyBMWVehicle) -> dict[str, Any]:
76  extra_attributes: dict[str, Any] = {}
77  for message in vehicle.check_control_messages.messages:
78  if (
79  message.description_short not in ALLOWED_CHECK_CONTROL_MESSAGE_KEYS
80  and message.description_short not in LOGGED_CHECK_CONTROL_MESSAGE_WARNINGS
81  ):
82  _LOGGER.warning(
83  "'%s' not an allowed check control message (%s)",
84  message.description_short,
85  message,
86  )
87  LOGGED_CHECK_CONTROL_MESSAGE_WARNINGS.add(message.description_short)
88  continue
89 
90  extra_attributes[message.description_short.lower()] = message.state.value
91  return extra_attributes
92 
93 
95  report: ConditionBasedService, unit_system: UnitSystem
96 ) -> dict[str, Any]:
97  result: dict[str, Any] = {}
98  service_type = report.service_type.lower()
99  result[service_type] = report.state.value
100  if report.due_date is not None:
101  result[f"{service_type}_date"] = report.due_date.strftime("%Y-%m-%d")
102  if report.due_distance.value and report.due_distance.unit:
103  distance = round(
104  unit_system.length(
105  report.due_distance.value,
106  UNIT_MAP.get(report.due_distance.unit, report.due_distance.unit),
107  )
108  )
109  result[f"{service_type}_distance"] = f"{distance} {unit_system.length_unit}"
110  return result
111 
112 
113 @dataclass(frozen=True, kw_only=True)
115  """Describes BMW binary_sensor entity."""
116 
117  value_fn: Callable[[MyBMWVehicle], bool]
118  attr_fn: Callable[[MyBMWVehicle, UnitSystem], dict[str, Any]] | None = None
119  is_available: Callable[[MyBMWVehicle], bool] = lambda v: v.is_lsc_enabled
120 
121 
122 SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = (
124  key="lids",
125  translation_key="lids",
126  device_class=BinarySensorDeviceClass.OPENING,
127  # device class opening: On means open, Off means closed
128  value_fn=lambda v: not v.doors_and_windows.all_lids_closed,
129  attr_fn=lambda v, u: {
130  lid.name: lid.state.value for lid in v.doors_and_windows.lids
131  },
132  ),
134  key="windows",
135  translation_key="windows",
136  device_class=BinarySensorDeviceClass.OPENING,
137  # device class opening: On means open, Off means closed
138  value_fn=lambda v: not v.doors_and_windows.all_windows_closed,
139  attr_fn=lambda v, u: {
140  window.name: window.state.value for window in v.doors_and_windows.windows
141  },
142  ),
144  key="door_lock_state",
145  translation_key="door_lock_state",
146  device_class=BinarySensorDeviceClass.LOCK,
147  # device class lock: On means unlocked, Off means locked
148  # Possible values: LOCKED, SECURED, SELECTIVE_LOCKED, UNLOCKED
149  value_fn=lambda v: v.doors_and_windows.door_lock_state
150  not in {LockState.LOCKED, LockState.SECURED},
151  attr_fn=lambda v, u: {
152  "door_lock_state": v.doors_and_windows.door_lock_state.value
153  },
154  ),
156  key="condition_based_services",
157  translation_key="condition_based_services",
158  device_class=BinarySensorDeviceClass.PROBLEM,
159  # device class problem: On means problem detected, Off means no problem
160  value_fn=lambda v: v.condition_based_services.is_service_required,
161  attr_fn=_condition_based_services,
162  ),
164  key="check_control_messages",
165  translation_key="check_control_messages",
166  device_class=BinarySensorDeviceClass.PROBLEM,
167  # device class problem: On means problem detected, Off means no problem
168  value_fn=lambda v: v.check_control_messages.has_check_control_messages,
169  attr_fn=lambda v, u: _check_control_messages(v),
170  ),
171  # electric
173  key="charging_status",
174  translation_key="charging_status",
175  device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
176  # device class power: On means power detected, Off means no power
177  value_fn=lambda v: v.fuel_and_battery.charging_status == ChargingState.CHARGING,
178  is_available=lambda v: v.has_electric_drivetrain,
179  ),
181  key="connection_status",
182  translation_key="connection_status",
183  device_class=BinarySensorDeviceClass.PLUG,
184  value_fn=lambda v: v.fuel_and_battery.is_charger_connected,
185  is_available=lambda v: v.has_electric_drivetrain,
186  ),
188  key="is_pre_entry_climatization_enabled",
189  translation_key="is_pre_entry_climatization_enabled",
190  value_fn=lambda v: v.charging_profile.is_pre_entry_climatization_enabled
191  if v.charging_profile
192  else False,
193  is_available=lambda v: v.has_electric_drivetrain,
194  ),
195 )
196 
197 
199  hass: HomeAssistant,
200  config_entry: BMWConfigEntry,
201  async_add_entities: AddEntitiesCallback,
202 ) -> None:
203  """Set up the BMW binary sensors from config entry."""
204  coordinator = config_entry.runtime_data.coordinator
205 
206  entities = [
207  BMWBinarySensor(coordinator, vehicle, description, hass.config.units)
208  for vehicle in coordinator.account.vehicles
209  for description in SENSOR_TYPES
210  if description.is_available(vehicle)
211  ]
212  async_add_entities(entities)
213 
214 
216  """Representation of a BMW vehicle binary sensor."""
217 
218  entity_description: BMWBinarySensorEntityDescription
219 
220  def __init__(
221  self,
222  coordinator: BMWDataUpdateCoordinator,
223  vehicle: MyBMWVehicle,
224  description: BMWBinarySensorEntityDescription,
225  unit_system: UnitSystem,
226  ) -> None:
227  """Initialize sensor."""
228  super().__init__(coordinator, vehicle)
229  self.entity_descriptionentity_description = description
230  self._unit_system_unit_system = unit_system
231  self._attr_unique_id_attr_unique_id = f"{vehicle.vin}-{description.key}"
232 
233  @callback
234  def _handle_coordinator_update(self) -> None:
235  """Handle updated data from the coordinator."""
236  _LOGGER.debug(
237  "Updating binary sensor '%s' of %s",
238  self.entity_descriptionentity_description.key,
239  self.vehiclevehicle.name,
240  )
241  self._attr_is_on_attr_is_on = self.entity_descriptionentity_description.value_fn(self.vehiclevehicle)
242 
243  if self.entity_descriptionentity_description.attr_fn:
244  self._attr_extra_state_attributes_attr_extra_state_attributes = self.entity_descriptionentity_description.attr_fn(
245  self.vehiclevehicle, self._unit_system_unit_system
246  )
247 
None __init__(self, BMWDataUpdateCoordinator coordinator, MyBMWVehicle vehicle, BMWBinarySensorEntityDescription description, UnitSystem unit_system)
dict[str, Any] _format_cbs_report(ConditionBasedService report, UnitSystem unit_system)
dict[str, Any] _condition_based_services(MyBMWVehicle vehicle, UnitSystem unit_system)
None async_setup_entry(HomeAssistant hass, BMWConfigEntry config_entry, AddEntitiesCallback async_add_entities)
dict[str, Any] _check_control_messages(MyBMWVehicle vehicle)