From 007f75cdcec774d430bcabae596ba80f64ed05dc Mon Sep 17 00:00:00 2001 From: John Pettitt Date: Thu, 6 Aug 2026 23:11:44 -0700 Subject: [PATCH] Add binary_sensor platform to Subaru (#177907) --- .../components/subaru/binary_sensor.py | 295 +++++ homeassistant/components/subaru/const.py | 5 + homeassistant/components/subaru/strings.json | 116 ++ tests/components/subaru/api_responses.py | 14 + .../subaru/snapshots/test_binary_sensor.ambr | 1072 +++++++++++++++++ .../subaru/snapshots/test_diagnostics.ambr | 40 + tests/components/subaru/test_binary_sensor.py | 464 +++++++ 7 files changed, 2006 insertions(+) create mode 100644 homeassistant/components/subaru/binary_sensor.py create mode 100644 tests/components/subaru/snapshots/test_binary_sensor.ambr create mode 100644 tests/components/subaru/test_binary_sensor.py diff --git a/homeassistant/components/subaru/binary_sensor.py b/homeassistant/components/subaru/binary_sensor.py new file mode 100644 index 000000000000..62507e3bb94e --- /dev/null +++ b/homeassistant/components/subaru/binary_sensor.py @@ -0,0 +1,295 @@ +"""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]) diff --git a/homeassistant/components/subaru/const.py b/homeassistant/components/subaru/const.py index d1a1662d6474..6f937ff6b3c6 100644 --- a/homeassistant/components/subaru/const.py +++ b/homeassistant/components/subaru/const.py @@ -25,6 +25,7 @@ VEHICLE_HAS_SAFETY_SERVICE = "has_safety" VEHICLE_LAST_UPDATE = "last_update" VEHICLE_STATUS = "vehicle_status" VEHICLE_HEALTH = "vehicle_health" +VEHICLE_FEATURES = "vehicle_features" # Synthetic keys for sensors that don't read a single field directly; used # as both unique_id suffix and translation_key, so they must stay stable @@ -38,9 +39,13 @@ API_GEN_1 = "g1" API_GEN_2 = "g2" API_GEN_3 = "g3" API_GEN_4 = "g4" +# Generations that report vehicle_status/vehicle_health data, used to gate +# binary_sensor entity creation. +GEN_2_AND_NEWER = (API_GEN_2, API_GEN_3, API_GEN_4) MANUFACTURER = "Subaru" PLATFORMS = [ + Platform.BINARY_SENSOR, Platform.BUTTON, Platform.DEVICE_TRACKER, Platform.LOCK, diff --git a/homeassistant/components/subaru/strings.json b/homeassistant/components/subaru/strings.json index fd8209f150ac..fdf4ff4dc54c 100644 --- a/homeassistant/components/subaru/strings.json +++ b/homeassistant/components/subaru/strings.json @@ -47,6 +47,122 @@ } }, "entity": { + "binary_sensor": { + "door_boot": { + "name": "Tailgate" + }, + "door_engine_hood": { + "name": "Hood" + }, + "door_front_left": { + "name": "Door front left" + }, + "door_front_right": { + "name": "Door front right" + }, + "door_rear_left": { + "name": "Door rear left" + }, + "door_rear_right": { + "name": "Door rear right" + }, + "ev_is_plugged_in": { + "name": "EV plug" + }, + "health_istrouble": { + "name": "Vehicle health" + }, + "is_charging": { + "name": "Charging" + }, + "lock_status_boot": { + "name": "Lock status tailgate" + }, + "lock_status_front_left": { + "name": "Lock status front left" + }, + "lock_status_front_right": { + "name": "Lock status front right" + }, + "lock_status_rear_left": { + "name": "Lock status rear left" + }, + "lock_status_rear_right": { + "name": "Lock status rear right" + }, + "mil_abs": { + "name": "ABS warning" + }, + "mil_atf": { + "name": "Transmission temperature warning" + }, + "mil_awd": { + "name": "AWD warning" + }, + "mil_bsdrct": { + "name": "Blind spot and rear cross traffic warning" + }, + "mil_cel": { + "name": "Check engine" + }, + "mil_ebd": { + "name": "Electronic brake force distribution warning" + }, + "mil_eol": { + "name": "Engine oil level warning" + }, + "mil_epas": { + "name": "Electric power steering warning" + }, + "mil_epb": { + "name": "Electric parking brake warning" + }, + "mil_ess": { + "name": "EyeSight warning" + }, + "mil_iss": { + "name": "Idle stop & start warning" + }, + "mil_opl": { + "name": "Oil pressure warning" + }, + "mil_rab": { + "name": "Reverse automatic braking warning" + }, + "mil_srh": { + "name": "Steering responsive headlights warning" + }, + "mil_srs": { + "name": "Airbag warning" + }, + "mil_tel": { + "name": "Telematics warning" + }, + "mil_tpms": { + "name": "Tire pressure warning" + }, + "mil_vdc": { + "name": "Vehicle dynamics control warning" + }, + "mil_wash": { + "name": "Washer fluid warning" + }, + "window_front_left": { + "name": "Window front left" + }, + "window_front_right": { + "name": "Window front right" + }, + "window_rear_left": { + "name": "Window rear left" + }, + "window_rear_right": { + "name": "Window rear right" + }, + "window_sunroof": { + "name": "Sunroof" + } + }, "button": { "remote_start": { "name": "Remote start" diff --git a/tests/components/subaru/api_responses.py b/tests/components/subaru/api_responses.py index c897ec328877..d0117def474f 100644 --- a/tests/components/subaru/api_responses.py +++ b/tests/components/subaru/api_responses.py @@ -8,6 +8,7 @@ from homeassistant.components.subaru.const import ( API_GEN_3, API_GEN_4, VEHICLE_API_GEN, + VEHICLE_FEATURES, VEHICLE_HAS_EV, VEHICLE_HAS_REMOTE_SERVICE, VEHICLE_HAS_REMOTE_START, @@ -92,6 +93,11 @@ VEHICLE_STATUS_EV = { "EV_STATE_OF_CHARGE_MODE": "EV_MODE", "EV_STATE_OF_CHARGE_PERCENT": 20, "EV_TIME_TO_FULLY_CHARGED_UTC": MOCK_DATETIME, + "LOCK_BOOT_STATUS": "LOCKED", + "LOCK_FRONT_LEFT_STATUS": "LOCKED", + "LOCK_FRONT_RIGHT_STATUS": "LOCKED", + "LOCK_REAR_LEFT_STATUS": "LOCKED", + "LOCK_REAR_RIGHT_STATUS": "LOCKED", "ODOMETER": 1234, "TIMESTAMP": 1595560000.0, "TRANSMISSION_MODE": "UNKNOWN", @@ -111,7 +117,15 @@ VEHICLE_STATUS_EV = { }, VEHICLE_HEALTH: { "RECOMMENDED_TIRE_PRESSURE": {"FRONT_TIRES": 35, "REAR_TIRES": 33}, + # subarulink ORs MIL ISTROUBLE into the top-level one, so it can't + # be False here while a feature below is True. + "ISTROUBLE": True, + "FEATURES": { + "TPMS_MIL": {"ISTROUBLE": False, "ONDATE": None}, + "CEL_MIL": {"ISTROUBLE": True, "ONDATE": None}, + }, }, + VEHICLE_FEATURES: ["TPMS_MIL", "CEL_MIL"], } diff --git a/tests/components/subaru/snapshots/test_binary_sensor.ambr b/tests/components/subaru/snapshots/test_binary_sensor.ambr new file mode 100644 index 000000000000..ef5cf7a2ce13 --- /dev/null +++ b/tests/components/subaru/snapshots/test_binary_sensor.ambr @@ -0,0 +1,1072 @@ +# serializer version: 1 +# name: test_all_entities[binary_sensor.test_vehicle_2_charging-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_vehicle_2_charging', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Charging', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Charging', + 'platform': 'subaru', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'is_charging', + 'unique_id': 'JF2ABCDE6L0000002_EV_CHARGER_STATE_TYPE', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_charging-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'battery_charging', + : 'test_vehicle_2 Charging', + }), + 'context': , + 'entity_id': 'binary_sensor.test_vehicle_2_charging', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_check_engine-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.test_vehicle_2_check_engine', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Check engine', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Check engine', + 'platform': 'subaru', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'mil_cel', + 'unique_id': 'JF2ABCDE6L0000002_CEL_MIL', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_check_engine-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'problem', + : 'test_vehicle_2 Check engine', + }), + 'context': , + 'entity_id': 'binary_sensor.test_vehicle_2_check_engine', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_door_front_left-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_vehicle_2_door_front_left', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Door front left', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Door front left', + 'platform': 'subaru', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'door_front_left', + 'unique_id': 'JF2ABCDE6L0000002_DOOR_FRONT_LEFT_POSITION', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_door_front_left-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'door', + : 'test_vehicle_2 Door front left', + }), + 'context': , + 'entity_id': 'binary_sensor.test_vehicle_2_door_front_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_door_front_right-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_vehicle_2_door_front_right', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Door front right', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Door front right', + 'platform': 'subaru', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'door_front_right', + 'unique_id': 'JF2ABCDE6L0000002_DOOR_FRONT_RIGHT_POSITION', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_door_front_right-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'door', + : 'test_vehicle_2 Door front right', + }), + 'context': , + 'entity_id': 'binary_sensor.test_vehicle_2_door_front_right', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_door_rear_left-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_vehicle_2_door_rear_left', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Door rear left', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Door rear left', + 'platform': 'subaru', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'door_rear_left', + 'unique_id': 'JF2ABCDE6L0000002_DOOR_REAR_LEFT_POSITION', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_door_rear_left-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'door', + : 'test_vehicle_2 Door rear left', + }), + 'context': , + 'entity_id': 'binary_sensor.test_vehicle_2_door_rear_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_door_rear_right-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_vehicle_2_door_rear_right', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Door rear right', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Door rear right', + 'platform': 'subaru', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'door_rear_right', + 'unique_id': 'JF2ABCDE6L0000002_DOOR_REAR_RIGHT_POSITION', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_door_rear_right-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'door', + : 'test_vehicle_2 Door rear right', + }), + 'context': , + 'entity_id': 'binary_sensor.test_vehicle_2_door_rear_right', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_ev_plug-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_vehicle_2_ev_plug', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'EV plug', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'EV plug', + 'platform': 'subaru', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'ev_is_plugged_in', + 'unique_id': 'JF2ABCDE6L0000002_EV_IS_PLUGGED_IN', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_ev_plug-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'plug', + : 'test_vehicle_2 EV plug', + }), + 'context': , + 'entity_id': 'binary_sensor.test_vehicle_2_ev_plug', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_hood-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_vehicle_2_hood', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Hood', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Hood', + 'platform': 'subaru', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'door_engine_hood', + 'unique_id': 'JF2ABCDE6L0000002_DOOR_ENGINE_HOOD_POSITION', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_hood-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'door', + : 'test_vehicle_2 Hood', + }), + 'context': , + 'entity_id': 'binary_sensor.test_vehicle_2_hood', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_lock_status_front_left-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_vehicle_2_lock_status_front_left', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Lock status front left', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Lock status front left', + 'platform': 'subaru', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'lock_status_front_left', + 'unique_id': 'JF2ABCDE6L0000002_LOCK_FRONT_LEFT_STATUS', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_lock_status_front_left-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'lock', + : 'test_vehicle_2 Lock status front left', + }), + 'context': , + 'entity_id': 'binary_sensor.test_vehicle_2_lock_status_front_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_lock_status_front_right-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_vehicle_2_lock_status_front_right', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Lock status front right', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Lock status front right', + 'platform': 'subaru', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'lock_status_front_right', + 'unique_id': 'JF2ABCDE6L0000002_LOCK_FRONT_RIGHT_STATUS', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_lock_status_front_right-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'lock', + : 'test_vehicle_2 Lock status front right', + }), + 'context': , + 'entity_id': 'binary_sensor.test_vehicle_2_lock_status_front_right', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_lock_status_rear_left-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_vehicle_2_lock_status_rear_left', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Lock status rear left', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Lock status rear left', + 'platform': 'subaru', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'lock_status_rear_left', + 'unique_id': 'JF2ABCDE6L0000002_LOCK_REAR_LEFT_STATUS', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_lock_status_rear_left-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'lock', + : 'test_vehicle_2 Lock status rear left', + }), + 'context': , + 'entity_id': 'binary_sensor.test_vehicle_2_lock_status_rear_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_lock_status_rear_right-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_vehicle_2_lock_status_rear_right', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Lock status rear right', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Lock status rear right', + 'platform': 'subaru', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'lock_status_rear_right', + 'unique_id': 'JF2ABCDE6L0000002_LOCK_REAR_RIGHT_STATUS', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_lock_status_rear_right-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'lock', + : 'test_vehicle_2 Lock status rear right', + }), + 'context': , + 'entity_id': 'binary_sensor.test_vehicle_2_lock_status_rear_right', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_lock_status_tailgate-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_vehicle_2_lock_status_tailgate', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Lock status tailgate', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Lock status tailgate', + 'platform': 'subaru', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'lock_status_boot', + 'unique_id': 'JF2ABCDE6L0000002_LOCK_BOOT_STATUS', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_lock_status_tailgate-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'lock', + : 'test_vehicle_2 Lock status tailgate', + }), + 'context': , + 'entity_id': 'binary_sensor.test_vehicle_2_lock_status_tailgate', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_sunroof-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_vehicle_2_sunroof', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Sunroof', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Sunroof', + 'platform': 'subaru', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'window_sunroof', + 'unique_id': 'JF2ABCDE6L0000002_WINDOW_SUNROOF_STATUS', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_sunroof-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'window', + : 'test_vehicle_2 Sunroof', + }), + 'context': , + 'entity_id': 'binary_sensor.test_vehicle_2_sunroof', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_tailgate-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_vehicle_2_tailgate', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Tailgate', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tailgate', + 'platform': 'subaru', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'door_boot', + 'unique_id': 'JF2ABCDE6L0000002_DOOR_BOOT_POSITION', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_tailgate-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'door', + : 'test_vehicle_2 Tailgate', + }), + 'context': , + 'entity_id': 'binary_sensor.test_vehicle_2_tailgate', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_tire_pressure_warning-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.test_vehicle_2_tire_pressure_warning', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Tire pressure warning', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Tire pressure warning', + 'platform': 'subaru', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'mil_tpms', + 'unique_id': 'JF2ABCDE6L0000002_TPMS_MIL', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_tire_pressure_warning-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'problem', + : 'test_vehicle_2 Tire pressure warning', + }), + 'context': , + 'entity_id': 'binary_sensor.test_vehicle_2_tire_pressure_warning', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_vehicle_health-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': , + 'entity_id': 'binary_sensor.test_vehicle_2_vehicle_health', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Vehicle health', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Vehicle health', + 'platform': 'subaru', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'health_istrouble', + 'unique_id': 'JF2ABCDE6L0000002_health_istrouble', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_vehicle_health-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'problem', + : 'test_vehicle_2 Vehicle health', + }), + 'context': , + 'entity_id': 'binary_sensor.test_vehicle_2_vehicle_health', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_window_front_left-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_vehicle_2_window_front_left', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Window front left', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Window front left', + 'platform': 'subaru', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'window_front_left', + 'unique_id': 'JF2ABCDE6L0000002_WINDOW_FRONT_LEFT_STATUS', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_window_front_left-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'window', + : 'test_vehicle_2 Window front left', + }), + 'context': , + 'entity_id': 'binary_sensor.test_vehicle_2_window_front_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_window_front_right-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_vehicle_2_window_front_right', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Window front right', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Window front right', + 'platform': 'subaru', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'window_front_right', + 'unique_id': 'JF2ABCDE6L0000002_WINDOW_FRONT_RIGHT_STATUS', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_window_front_right-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'window', + : 'test_vehicle_2 Window front right', + }), + 'context': , + 'entity_id': 'binary_sensor.test_vehicle_2_window_front_right', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_window_rear_left-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_vehicle_2_window_rear_left', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Window rear left', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Window rear left', + 'platform': 'subaru', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'window_rear_left', + 'unique_id': 'JF2ABCDE6L0000002_WINDOW_REAR_LEFT_STATUS', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_window_rear_left-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'window', + : 'test_vehicle_2 Window rear left', + }), + 'context': , + 'entity_id': 'binary_sensor.test_vehicle_2_window_rear_left', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_window_rear_right-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.test_vehicle_2_window_rear_right', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Window rear right', + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Window rear right', + 'platform': 'subaru', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'window_rear_right', + 'unique_id': 'JF2ABCDE6L0000002_WINDOW_REAR_RIGHT_STATUS', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[binary_sensor.test_vehicle_2_window_rear_right-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : 'window', + : 'test_vehicle_2 Window rear right', + }), + 'context': , + 'entity_id': 'binary_sensor.test_vehicle_2_window_rear_right', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/subaru/snapshots/test_diagnostics.ambr b/tests/components/subaru/snapshots/test_diagnostics.ambr index b6cf9336d2f1..f1547e4b8533 100644 --- a/tests/components/subaru/snapshots/test_diagnostics.ambr +++ b/tests/components/subaru/snapshots/test_diagnostics.ambr @@ -10,7 +10,22 @@ }), 'data': list([ dict({ + 'vehicle_features': list([ + 'TPMS_MIL', + 'CEL_MIL', + ]), 'vehicle_health': dict({ + 'FEATURES': dict({ + 'CEL_MIL': dict({ + 'ISTROUBLE': True, + 'ONDATE': None, + }), + 'TPMS_MIL': dict({ + 'ISTROUBLE': False, + 'ONDATE': None, + }), + }), + 'ISTROUBLE': True, 'RECOMMENDED_TIRE_PRESSURE': dict({ 'FRONT_TIRES': 35, 'REAR_TIRES': 33, @@ -34,6 +49,11 @@ 'EV_STATE_OF_CHARGE_PERCENT': 20, 'EV_TIME_TO_FULLY_CHARGED_UTC': '2020-07-24T03:06:40+00:00', 'LATITUDE': '**REDACTED**', + 'LOCK_BOOT_STATUS': 'LOCKED', + 'LOCK_FRONT_LEFT_STATUS': 'LOCKED', + 'LOCK_FRONT_RIGHT_STATUS': 'LOCKED', + 'LOCK_REAR_LEFT_STATUS': 'LOCKED', + 'LOCK_REAR_RIGHT_STATUS': 'LOCKED', 'LONGITUDE': '**REDACTED**', 'ODOMETER': '**REDACTED**', 'TIMESTAMP': 1595560000.0, @@ -67,7 +87,22 @@ 'username': '**REDACTED**', }), 'data': dict({ + 'vehicle_features': list([ + 'TPMS_MIL', + 'CEL_MIL', + ]), 'vehicle_health': dict({ + 'FEATURES': dict({ + 'CEL_MIL': dict({ + 'ISTROUBLE': True, + 'ONDATE': None, + }), + 'TPMS_MIL': dict({ + 'ISTROUBLE': False, + 'ONDATE': None, + }), + }), + 'ISTROUBLE': True, 'RECOMMENDED_TIRE_PRESSURE': dict({ 'FRONT_TIRES': 35, 'REAR_TIRES': 33, @@ -91,6 +126,11 @@ 'EV_STATE_OF_CHARGE_PERCENT': 20, 'EV_TIME_TO_FULLY_CHARGED_UTC': '2020-07-24T03:06:40+00:00', 'LATITUDE': '**REDACTED**', + 'LOCK_BOOT_STATUS': 'LOCKED', + 'LOCK_FRONT_LEFT_STATUS': 'LOCKED', + 'LOCK_FRONT_RIGHT_STATUS': 'LOCKED', + 'LOCK_REAR_LEFT_STATUS': 'LOCKED', + 'LOCK_REAR_RIGHT_STATUS': 'LOCKED', 'LONGITUDE': '**REDACTED**', 'ODOMETER': '**REDACTED**', 'TIMESTAMP': 1595560000.0, diff --git a/tests/components/subaru/test_binary_sensor.py b/tests/components/subaru/test_binary_sensor.py new file mode 100644 index 000000000000..00eee3570502 --- /dev/null +++ b/tests/components/subaru/test_binary_sensor.py @@ -0,0 +1,464 @@ +"""Test Subaru binary sensors.""" + +import copy +from unittest.mock import patch + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.subaru.binary_sensor import ( + BINARY_SENSORS, + EV_CHARGING_BINARY_SENSOR, + EV_PLUG_BINARY_SENSOR, + LOCK_STATUS_KEYS, + MIL_TRANSLATION_KEYS, + OVERALL_HEALTH_BINARY_SENSOR, +) +from homeassistant.components.subaru.const import DOMAIN, VEHICLE_STATUS +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ( + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .api_responses import ( + TEST_VIN_1_G1, + TEST_VIN_2_EV, + TEST_VIN_3_G3, + TEST_VIN_4_G4, + VEHICLE_DATA, + VEHICLE_STATUS_EV, + VEHICLE_STATUS_G3, +) +from .conftest import setup_subaru_config_entry + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_all_entities( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + subaru_config_entry: MockConfigEntry, +) -> None: + """Snapshot all binary sensors created for an EV vehicle.""" + with patch( + "homeassistant.components.subaru.PLATFORMS", + [Platform.BINARY_SENSOR], + ): + await setup_subaru_config_entry(hass, subaru_config_entry) + await snapshot_platform( + hass, entity_registry, snapshot, subaru_config_entry.entry_id + ) + + +@pytest.mark.parametrize("feature", ["TPMS_MIL", "CEL_MIL"]) +@pytest.mark.usefixtures("ev_entry") +async def test_mil_entities_disabled_by_default( + entity_registry: er.EntityRegistry, + feature: str, +) -> None: + """MIL entities are created for reported MIL features and disabled by default.""" + entity_id = entity_registry.async_get_entity_id( + BINARY_SENSOR_DOMAIN, DOMAIN, f"{TEST_VIN_2_EV}_{feature}" + ) + assert entity_id is not None + entry = entity_registry.async_get(entity_id) + assert entry is not None + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + assert entry.translation_key == MIL_TRANSLATION_KEYS[feature] + + +@pytest.mark.parametrize( + "key", + [desc.key for desc in BINARY_SENSORS] + + [OVERALL_HEALTH_BINARY_SENSOR.key, EV_PLUG_BINARY_SENSOR.key], +) +async def test_no_binary_sensors_for_g1( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + subaru_config_entry: MockConfigEntry, + key: str, +) -> None: + """Gen1 vehicles do not get any binary sensors (no door/lock/health data).""" + await setup_subaru_config_entry( + hass, + subaru_config_entry, + vehicle_list=[TEST_VIN_1_G1], + vehicle_data=VEHICLE_DATA[TEST_VIN_1_G1], + ) + assert ( + entity_registry.async_get_entity_id( + BINARY_SENSOR_DOMAIN, DOMAIN, f"{TEST_VIN_1_G1}_{key}" + ) + is None + ) + + +async def test_no_ev_plug_binary_sensor_for_g3( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + subaru_config_entry: MockConfigEntry, +) -> None: + """Non-EV vehicles do not get the EV plug binary sensor.""" + await setup_subaru_config_entry( + hass, + subaru_config_entry, + vehicle_list=[TEST_VIN_3_G3], + vehicle_data=VEHICLE_DATA[TEST_VIN_3_G3], + vehicle_status=VEHICLE_STATUS_G3, + ) + assert ( + entity_registry.async_get_entity_id( + BINARY_SENSOR_DOMAIN, + DOMAIN, + f"{TEST_VIN_3_G3}_{EV_PLUG_BINARY_SENSOR.key}", + ) + is None + ) + + +@pytest.mark.parametrize("key", list(LOCK_STATUS_KEYS)) +async def test_no_lock_sensors_when_unsupported( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + subaru_config_entry: MockConfigEntry, + key: str, +) -> None: + """Lock sensors aren't created for a vehicle whose status omits them. + + VEHICLE_STATUS_G3 has no LOCK_* keys, matching a real vehicle without + lock-status support -- subarulink omits these fields entirely rather + than reporting them as unknown. + """ + await setup_subaru_config_entry( + hass, + subaru_config_entry, + vehicle_list=[TEST_VIN_3_G3], + vehicle_data=VEHICLE_DATA[TEST_VIN_3_G3], + vehicle_status=VEHICLE_STATUS_G3, + ) + assert ( + entity_registry.async_get_entity_id( + BINARY_SENSOR_DOMAIN, DOMAIN, f"{TEST_VIN_3_G3}_{key}" + ) + is None + ) + + +@pytest.mark.parametrize("not_equipped", ["NOT_EQUIPPED", "not_equipped"]) +async def test_door_not_equipped_gets_no_entity( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + subaru_config_entry: MockConfigEntry, + not_equipped: str, +) -> None: + """A door reported as NOT_EQUIPPED gets no entity, unlike a failed fetch. + + Unlike windows/locks, doors are always present in vehicle_status, so + a per-door NOT_EQUIPPED value (not the key's absence) is what signals + the trim genuinely lacks that sensor. Checked case-insensitively. + """ + vehicle_status = copy.deepcopy(VEHICLE_STATUS_EV) + vehicle_status[VEHICLE_STATUS]["DOOR_ENGINE_HOOD_POSITION"] = not_equipped + await setup_subaru_config_entry( + hass, subaru_config_entry, vehicle_status=vehicle_status + ) + assert ( + entity_registry.async_get_entity_id( + BINARY_SENSOR_DOMAIN, + DOMAIN, + f"{TEST_VIN_2_EV}_DOOR_ENGINE_HOOD_POSITION", + ) + is None + ) + assert ( + entity_registry.async_get_entity_id( + BINARY_SENSOR_DOMAIN, + DOMAIN, + f"{TEST_VIN_2_EV}_DOOR_FRONT_LEFT_POSITION", + ) + is not None + ) + + +async def test_no_ev_charging_sensor_when_unsupported( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + subaru_config_entry: MockConfigEntry, +) -> None: + """EV charging isn't created for an EV that doesn't report it, unlike EV plug.""" + vehicle_status = copy.deepcopy(VEHICLE_STATUS_EV) + del vehicle_status[VEHICLE_STATUS]["EV_CHARGER_STATE_TYPE"] + await setup_subaru_config_entry( + hass, subaru_config_entry, vehicle_status=vehicle_status + ) + assert ( + entity_registry.async_get_entity_id( + BINARY_SENSOR_DOMAIN, + DOMAIN, + f"{TEST_VIN_2_EV}_{EV_CHARGING_BINARY_SENSOR.key}", + ) + is None + ) + assert ( + entity_registry.async_get_entity_id( + BINARY_SENSOR_DOMAIN, + DOMAIN, + f"{TEST_VIN_2_EV}_{EV_PLUG_BINARY_SENSOR.key}", + ) + is not None + ) + + +async def test_overall_health_unknown_without_vehicle_health( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + subaru_config_entry: MockConfigEntry, +) -> None: + """Overall vehicle health is `unknown` when the API has not yet returned health data.""" + await setup_subaru_config_entry( + hass, + subaru_config_entry, + vehicle_list=[TEST_VIN_3_G3], + vehicle_data=VEHICLE_DATA[TEST_VIN_3_G3], + vehicle_status=VEHICLE_STATUS_G3, + ) + overall = entity_registry.async_get_entity_id( + BINARY_SENSOR_DOMAIN, DOMAIN, f"{TEST_VIN_3_G3}_health_istrouble" + ) + assert overall is not None + state = hass.states.get(overall) + assert state is not None + assert state.state == STATE_UNKNOWN + + +async def test_binary_sensors_created_for_g4( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + subaru_config_entry: MockConfigEntry, +) -> None: + """Gen4 vehicles get binary sensors, same as Gen2/Gen3.""" + await setup_subaru_config_entry( + hass, + subaru_config_entry, + vehicle_list=[TEST_VIN_4_G4], + vehicle_data=VEHICLE_DATA[TEST_VIN_4_G4], + vehicle_status=VEHICLE_STATUS_G3, + ) + assert ( + entity_registry.async_get_entity_id( + BINARY_SENSOR_DOMAIN, DOMAIN, f"{TEST_VIN_4_G4}_health_istrouble" + ) + is not None + ) + + +@pytest.mark.usefixtures("ev_entry") +async def test_ev_charging_disabled_by_default( + entity_registry: er.EntityRegistry, +) -> None: + """The EV charging sensor is disabled by default, unlike the EV plug sensor.""" + entity_id = entity_registry.async_get_entity_id( + BINARY_SENSOR_DOMAIN, + DOMAIN, + f"{TEST_VIN_2_EV}_{EV_CHARGING_BINARY_SENSOR.key}", + ) + assert entity_id is not None + entry = entity_registry.async_get(entity_id) + assert entry is not None + assert entry.disabled_by is er.RegistryEntryDisabler.INTEGRATION + + +@pytest.mark.parametrize("key", ["health_istrouble", "DOOR_FRONT_LEFT_POSITION"]) +async def test_entities_unavailable_when_vehicle_data_fetch_fails( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + subaru_config_entry: MockConfigEntry, + key: str, +) -> None: + """Setup survives a vehicle whose data fetch fails; its entities go unavailable. + + _refresh_subaru_data only adds coordinator.data[vin] when the per-vehicle + API call succeeds, so a failure must not crash setup for descriptions + that are created unconditionally (e.g. the door/health sensors). + """ + await setup_subaru_config_entry(hass, subaru_config_entry, vehicle_status={}) + assert subaru_config_entry.state is ConfigEntryState.LOADED + + entity_id = entity_registry.async_get_entity_id( + BINARY_SENSOR_DOMAIN, DOMAIN, f"{TEST_VIN_2_EV}_{key}" + ) + assert entity_id is not None + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_UNAVAILABLE + + +async def test_no_window_or_lock_entities_when_vehicle_data_fetch_fails( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + subaru_config_entry: MockConfigEntry, +) -> None: + """Unlike doors, windows/locks aren't created when the initial fetch fails.""" + await setup_subaru_config_entry(hass, subaru_config_entry, vehicle_status={}) + assert ( + entity_registry.async_get_entity_id( + BINARY_SENSOR_DOMAIN, DOMAIN, f"{TEST_VIN_2_EV}_WINDOW_FRONT_LEFT_STATUS" + ) + is None + ) + + +@pytest.mark.parametrize( + ("status", "expected_state"), + [ + ("CLOSED", STATE_OFF), + ("CLOSE", STATE_OFF), + ("closed", STATE_OFF), + ("OPEN", STATE_ON), + ("VENTED", STATE_ON), + ("UNKNOWN", STATE_UNKNOWN), + ("UNAVAILABLE", STATE_UNKNOWN), + ], +) +async def test_door_state( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + subaru_config_entry: MockConfigEntry, + status: str, + expected_state: str, +) -> None: + """Door state reflects the raw status, case-insensitively; sentinels are unknown.""" + vehicle_status = copy.deepcopy(VEHICLE_STATUS_EV) + vehicle_status[VEHICLE_STATUS]["DOOR_FRONT_LEFT_POSITION"] = status + await setup_subaru_config_entry( + hass, subaru_config_entry, vehicle_status=vehicle_status + ) + entity_id = entity_registry.async_get_entity_id( + BINARY_SENSOR_DOMAIN, DOMAIN, f"{TEST_VIN_2_EV}_DOOR_FRONT_LEFT_POSITION" + ) + assert entity_id is not None + state = hass.states.get(entity_id) + assert state is not None + assert state.state == expected_state + + +@pytest.mark.parametrize( + ("status", "expected_state"), + [ + ("LOCKED", STATE_OFF), + ("UNLOCKED", STATE_ON), + ], +) +async def test_lock_sensor_state( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + subaru_config_entry: MockConfigEntry, + status: str, + expected_state: str, +) -> None: + """Lock-status sensor is on when unlocked.""" + vehicle_status = copy.deepcopy(VEHICLE_STATUS_EV) + vehicle_status[VEHICLE_STATUS]["LOCK_FRONT_LEFT_STATUS"] = status + await setup_subaru_config_entry( + hass, subaru_config_entry, vehicle_status=vehicle_status + ) + entity_id = entity_registry.async_get_entity_id( + BINARY_SENSOR_DOMAIN, DOMAIN, f"{TEST_VIN_2_EV}_LOCK_FRONT_LEFT_STATUS" + ) + assert entity_id is not None + state = hass.states.get(entity_id) + assert state is not None + assert state.state == expected_state + + +@pytest.mark.parametrize( + ("status", "expected_state"), + [ + ("CHARGING", STATE_ON), + ("LOCKED_CONNECTED", STATE_ON), + ("UNLOCKED_CONNECTED", STATE_ON), + ("UNPLUGGED", STATE_OFF), + ], +) +async def test_ev_plug_state( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + subaru_config_entry: MockConfigEntry, + status: str, + expected_state: str, +) -> None: + """EV plug sensor is on for any documented connected state.""" + vehicle_status = copy.deepcopy(VEHICLE_STATUS_EV) + vehicle_status[VEHICLE_STATUS]["EV_IS_PLUGGED_IN"] = status + await setup_subaru_config_entry( + hass, subaru_config_entry, vehicle_status=vehicle_status + ) + entity_id = entity_registry.async_get_entity_id( + BINARY_SENSOR_DOMAIN, DOMAIN, f"{TEST_VIN_2_EV}_EV_IS_PLUGGED_IN" + ) + assert entity_id is not None + state = hass.states.get(entity_id) + assert state is not None + assert state.state == expected_state + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +@pytest.mark.parametrize( + ("status", "expected_state"), + [ + ("CHARGING", STATE_ON), + ("NOT_CHARGING", STATE_OFF), + ("UNPLUGGED", STATE_OFF), + ], +) +async def test_ev_charging_state( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + subaru_config_entry: MockConfigEntry, + status: str, + expected_state: str, +) -> None: + """EV charging sensor is on only while actively CHARGING.""" + vehicle_status = copy.deepcopy(VEHICLE_STATUS_EV) + vehicle_status[VEHICLE_STATUS]["EV_CHARGER_STATE_TYPE"] = status + await setup_subaru_config_entry( + hass, subaru_config_entry, vehicle_status=vehicle_status + ) + entity_id = entity_registry.async_get_entity_id( + BINARY_SENSOR_DOMAIN, DOMAIN, f"{TEST_VIN_2_EV}_EV_CHARGER_STATE_TYPE" + ) + assert entity_id is not None + state = hass.states.get(entity_id) + assert state is not None + assert state.state == expected_state + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default", "ev_entry") +async def test_mil_sensor_state( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, +) -> None: + """MIL sensor reflects the per-feature ISTROUBLE flag.""" + on_entity = entity_registry.async_get_entity_id( + BINARY_SENSOR_DOMAIN, DOMAIN, f"{TEST_VIN_2_EV}_CEL_MIL" + ) + off_entity = entity_registry.async_get_entity_id( + BINARY_SENSOR_DOMAIN, DOMAIN, f"{TEST_VIN_2_EV}_TPMS_MIL" + ) + assert on_entity is not None + assert off_entity is not None + on_state = hass.states.get(on_entity) + off_state = hass.states.get(off_entity) + assert on_state is not None + assert off_state is not None + assert on_state.state == STATE_ON + assert off_state.state == STATE_OFF