mirror of
https://github.com/home-assistant/core.git
synced 2026-08-14 17:23:33 +01:00
296 lines
11 KiB
Python
296 lines
11 KiB
Python
"""Support for Subaru binary sensors."""
|
|
|
|
from collections.abc import Callable
|
|
from dataclasses import dataclass
|
|
from functools import partial
|
|
from typing import Any, override
|
|
|
|
from homeassistant.components.binary_sensor import (
|
|
BinarySensorDeviceClass,
|
|
BinarySensorEntity,
|
|
BinarySensorEntityDescription,
|
|
)
|
|
from homeassistant.const import EntityCategory
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|
|
|
from .const import (
|
|
GEN_2_AND_NEWER,
|
|
VEHICLE_API_GEN,
|
|
VEHICLE_FEATURES,
|
|
VEHICLE_HAS_EV,
|
|
VEHICLE_HEALTH,
|
|
VEHICLE_STATUS,
|
|
VEHICLE_VIN,
|
|
)
|
|
from .coordinator import SubaruConfigEntry, SubaruDataUpdateCoordinator
|
|
from .entity import SubaruCoordinatorEntity
|
|
|
|
# Keys returned by subarulink controller.get_data() inside vehicle_status.
|
|
DOOR_POSITION_KEYS: dict[str, str] = {
|
|
"DOOR_FRONT_LEFT_POSITION": "door_front_left",
|
|
"DOOR_FRONT_RIGHT_POSITION": "door_front_right",
|
|
"DOOR_REAR_LEFT_POSITION": "door_rear_left",
|
|
"DOOR_REAR_RIGHT_POSITION": "door_rear_right",
|
|
"DOOR_BOOT_POSITION": "door_boot",
|
|
"DOOR_ENGINE_HOOD_POSITION": "door_engine_hood",
|
|
}
|
|
WINDOW_STATUS_KEYS: dict[str, str] = {
|
|
"WINDOW_FRONT_LEFT_STATUS": "window_front_left",
|
|
"WINDOW_FRONT_RIGHT_STATUS": "window_front_right",
|
|
"WINDOW_REAR_LEFT_STATUS": "window_rear_left",
|
|
"WINDOW_REAR_RIGHT_STATUS": "window_rear_right",
|
|
"WINDOW_SUNROOF_STATUS": "window_sunroof",
|
|
}
|
|
LOCK_STATUS_KEYS: dict[str, str] = {
|
|
"LOCK_FRONT_LEFT_STATUS": "lock_status_front_left",
|
|
"LOCK_FRONT_RIGHT_STATUS": "lock_status_front_right",
|
|
"LOCK_REAR_LEFT_STATUS": "lock_status_rear_left",
|
|
"LOCK_REAR_RIGHT_STATUS": "lock_status_rear_right",
|
|
"LOCK_BOOT_STATUS": "lock_status_boot",
|
|
}
|
|
|
|
# EV_IS_PLUGGED_IN values meaning connected; other known values mean not
|
|
# connected.
|
|
EV_PLUGGED_IN_STATES = frozenset({"CHARGING", "LOCKED_CONNECTED", "UNLOCKED_CONNECTED"})
|
|
API_KEY_EV_IS_PLUGGED_IN = "EV_IS_PLUGGED_IN"
|
|
API_KEY_EV_CHARGER_STATE_TYPE = "EV_CHARGER_STATE_TYPE"
|
|
EV_CHARGING_STATE = "CHARGING"
|
|
|
|
# vehicle_health response shape (see integration debug diagnostics).
|
|
HEALTH_ISTROUBLE = "ISTROUBLE"
|
|
HEALTH_FEATURES = "FEATURES"
|
|
|
|
# Subaru MIL (Malfunction Indicator Lamp) feature codes -> translation keys.
|
|
# ATF_MIL is a transmission temperature warning, not fluid level.
|
|
MIL_TRANSLATION_KEYS: dict[str, str] = {
|
|
"SRS_MIL": "mil_srs",
|
|
"AWD_MIL": "mil_awd",
|
|
"ABS_MIL": "mil_abs",
|
|
"ATF_MIL": "mil_atf",
|
|
"BSDRCT_MIL": "mil_bsdrct",
|
|
"CEL_MIL": "mil_cel",
|
|
"EBD_MIL": "mil_ebd",
|
|
"EPB_MIL": "mil_epb",
|
|
"EOL_MIL": "mil_eol",
|
|
"ESS_MIL": "mil_ess",
|
|
"ISS_MIL": "mil_iss",
|
|
"OPL_MIL": "mil_opl",
|
|
"EPAS_MIL": "mil_epas",
|
|
"RAB_MIL": "mil_rab",
|
|
"TEL_MIL": "mil_tel",
|
|
"TPMS_MIL": "mil_tpms",
|
|
"VDC_MIL": "mil_vdc",
|
|
"WASH_MIL": "mil_wash",
|
|
"SRH_MIL": "mil_srh",
|
|
}
|
|
|
|
# "CLOSED" (doors) or "CLOSE" (windows) means closed.
|
|
OPENING_CLOSED_VALUES = frozenset({"CLOSED", "CLOSE"})
|
|
# Sentinel values meaning "no data"; compared case-insensitively.
|
|
UNKNOWN_STATUSES = frozenset({"UNKNOWN", "UNAVAILABLE", "NOT_EQUIPPED"})
|
|
|
|
|
|
@dataclass(frozen=True, kw_only=True)
|
|
class SubaruBinarySensorEntityDescription(BinarySensorEntityDescription):
|
|
"""Describes a Subaru binary sensor entity."""
|
|
|
|
is_on_fn: Callable[[dict[str, Any]], bool | None]
|
|
|
|
|
|
def _vehicle_status_value(vehicle_data: dict[str, Any], api_key: str) -> str | None:
|
|
"""Return the normalized vehicle_status value for api_key, or None if missing/unknown."""
|
|
status = (vehicle_data.get(VEHICLE_STATUS) or {}).get(api_key)
|
|
if status is None:
|
|
return None
|
|
status = status.upper()
|
|
return None if status in UNKNOWN_STATUSES else status
|
|
|
|
|
|
def _opening_is_on(vehicle_data: dict[str, Any], api_key: str) -> bool | None:
|
|
"""Whether a door/window field is open."""
|
|
value = _vehicle_status_value(vehicle_data, api_key)
|
|
return None if value is None else value not in OPENING_CLOSED_VALUES
|
|
|
|
|
|
def _lock_is_on(vehicle_data: dict[str, Any], api_key: str) -> bool | None:
|
|
"""Whether a lock field is unlocked."""
|
|
value = _vehicle_status_value(vehicle_data, api_key)
|
|
return None if value is None else value != "LOCKED"
|
|
|
|
|
|
def _mil_trouble(vehicle_data: dict[str, Any], feature: str) -> bool | None:
|
|
"""Return vehicle_health.FEATURES[feature].ISTROUBLE, or None if not reported."""
|
|
features = (vehicle_data.get(VEHICLE_HEALTH) or {}).get(HEALTH_FEATURES) or {}
|
|
feature_health = features.get(feature)
|
|
if not feature_health or HEALTH_ISTROUBLE not in feature_health:
|
|
return None
|
|
return bool(feature_health[HEALTH_ISTROUBLE])
|
|
|
|
|
|
# Static descriptions for entities that are created for every Gen2+ vehicle.
|
|
# MIL diagnostics are built dynamically below based on vehicle_features.
|
|
BINARY_SENSORS: tuple[SubaruBinarySensorEntityDescription, ...] = (
|
|
*(
|
|
SubaruBinarySensorEntityDescription(
|
|
key=api_key,
|
|
translation_key=trans_key,
|
|
device_class=BinarySensorDeviceClass.DOOR,
|
|
is_on_fn=partial(_opening_is_on, api_key=api_key),
|
|
)
|
|
for api_key, trans_key in DOOR_POSITION_KEYS.items()
|
|
),
|
|
*(
|
|
SubaruBinarySensorEntityDescription(
|
|
key=api_key,
|
|
translation_key=trans_key,
|
|
device_class=BinarySensorDeviceClass.WINDOW,
|
|
is_on_fn=partial(_opening_is_on, api_key=api_key),
|
|
)
|
|
for api_key, trans_key in WINDOW_STATUS_KEYS.items()
|
|
),
|
|
*(
|
|
SubaruBinarySensorEntityDescription(
|
|
key=api_key,
|
|
translation_key=trans_key,
|
|
device_class=BinarySensorDeviceClass.LOCK,
|
|
is_on_fn=partial(_lock_is_on, api_key=api_key),
|
|
)
|
|
for api_key, trans_key in LOCK_STATUS_KEYS.items()
|
|
),
|
|
)
|
|
|
|
OVERALL_HEALTH_BINARY_SENSOR = SubaruBinarySensorEntityDescription(
|
|
key="health_istrouble",
|
|
translation_key="health_istrouble",
|
|
device_class=BinarySensorDeviceClass.PROBLEM,
|
|
entity_category=EntityCategory.DIAGNOSTIC,
|
|
is_on_fn=lambda d: (
|
|
None
|
|
if not (health := d.get(VEHICLE_HEALTH)) or HEALTH_ISTROUBLE not in health
|
|
else bool(health[HEALTH_ISTROUBLE])
|
|
),
|
|
)
|
|
|
|
EV_PLUG_BINARY_SENSOR = SubaruBinarySensorEntityDescription(
|
|
key=API_KEY_EV_IS_PLUGGED_IN,
|
|
translation_key="ev_is_plugged_in",
|
|
device_class=BinarySensorDeviceClass.PLUG,
|
|
is_on_fn=lambda d: (
|
|
None
|
|
if (v := _vehicle_status_value(d, API_KEY_EV_IS_PLUGGED_IN)) is None
|
|
else v in EV_PLUGGED_IN_STATES
|
|
),
|
|
)
|
|
|
|
EV_CHARGING_BINARY_SENSOR = SubaruBinarySensorEntityDescription(
|
|
key=API_KEY_EV_CHARGER_STATE_TYPE,
|
|
translation_key="is_charging",
|
|
device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
|
|
entity_registry_enabled_default=False,
|
|
is_on_fn=lambda d: (
|
|
None
|
|
if (v := _vehicle_status_value(d, API_KEY_EV_CHARGER_STATE_TYPE)) is None
|
|
else v == EV_CHARGING_STATE
|
|
),
|
|
)
|
|
|
|
|
|
def _build_mil_descriptions(
|
|
features: list[str],
|
|
) -> list[SubaruBinarySensorEntityDescription]:
|
|
"""Return MIL descriptions for MIL feature codes that the vehicle reports.
|
|
|
|
Built once at setup; a MIL code that starts appearing later (partial
|
|
first poll, or a code only reported once triggered) needs a reload.
|
|
"""
|
|
return [
|
|
SubaruBinarySensorEntityDescription(
|
|
key=feature,
|
|
translation_key=MIL_TRANSLATION_KEYS[feature],
|
|
device_class=BinarySensorDeviceClass.PROBLEM,
|
|
entity_category=EntityCategory.DIAGNOSTIC,
|
|
# Disabled by default to avoid ~19 mostly-off entries per
|
|
# vehicle; the overall health rollup stays enabled.
|
|
entity_registry_enabled_default=False,
|
|
is_on_fn=partial(_mil_trouble, feature=feature),
|
|
)
|
|
for feature in features
|
|
if feature in MIL_TRANSLATION_KEYS
|
|
]
|
|
|
|
|
|
def _has_data(
|
|
description: SubaruBinarySensorEntityDescription, vehicle_status: dict[str, Any]
|
|
) -> bool:
|
|
"""Whether a door/window/lock description should be created.
|
|
|
|
Doors report even on an empty vehicle_status (a failed fetch), so
|
|
they're excluded only when explicitly NOT_EQUIPPED. Windows/locks/EV
|
|
fields are omitted entirely when unsupported, so presence decides.
|
|
"""
|
|
if description.key in DOOR_POSITION_KEYS:
|
|
return vehicle_status.get(description.key, "").upper() != "NOT_EQUIPPED"
|
|
return description.key in vehicle_status
|
|
|
|
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant,
|
|
config_entry: SubaruConfigEntry,
|
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
|
) -> None:
|
|
"""Set up the Subaru binary sensors by config_entry."""
|
|
coordinator = config_entry.runtime_data.coordinator
|
|
vehicle_info = config_entry.runtime_data.vehicles
|
|
|
|
entities: list[SubaruBinarySensor] = []
|
|
for info in vehicle_info.values():
|
|
# Doors/windows/locks/health are only reported on Gen2+ vehicles.
|
|
if info[VEHICLE_API_GEN] not in GEN_2_AND_NEWER:
|
|
continue
|
|
vehicle_data = (coordinator.data or {}).get(info[VEHICLE_VIN]) or {}
|
|
vehicle_status = vehicle_data.get(VEHICLE_STATUS) or {}
|
|
|
|
descriptions: list[SubaruBinarySensorEntityDescription] = [
|
|
description
|
|
for description in BINARY_SENSORS
|
|
if _has_data(description, vehicle_status)
|
|
]
|
|
descriptions.append(OVERALL_HEALTH_BINARY_SENSOR)
|
|
if info[VEHICLE_HAS_EV]:
|
|
if EV_PLUG_BINARY_SENSOR.key in vehicle_status:
|
|
descriptions.append(EV_PLUG_BINARY_SENSOR)
|
|
if EV_CHARGING_BINARY_SENSOR.key in vehicle_status:
|
|
descriptions.append(EV_CHARGING_BINARY_SENSOR)
|
|
|
|
features = vehicle_data.get(VEHICLE_FEATURES) or []
|
|
descriptions.extend(_build_mil_descriptions(features))
|
|
|
|
entities.extend(
|
|
SubaruBinarySensor(info, coordinator, description)
|
|
for description in descriptions
|
|
)
|
|
async_add_entities(entities)
|
|
|
|
|
|
class SubaruBinarySensor(SubaruCoordinatorEntity, BinarySensorEntity):
|
|
"""Representation of a Subaru binary sensor."""
|
|
|
|
entity_description: SubaruBinarySensorEntityDescription
|
|
|
|
def __init__(
|
|
self,
|
|
vehicle_info: dict[str, Any],
|
|
coordinator: SubaruDataUpdateCoordinator,
|
|
description: SubaruBinarySensorEntityDescription,
|
|
) -> None:
|
|
"""Initialize the binary sensor."""
|
|
super().__init__(vehicle_info, coordinator, description.key)
|
|
self.entity_description = description
|
|
|
|
@property
|
|
@override
|
|
def is_on(self) -> bool | None:
|
|
"""Return True if the sensor is on (open / unlocked / has trouble)."""
|
|
return self.entity_description.is_on_fn(self.coordinator.data[self.vin])
|