mirror of
https://github.com/home-assistant/core.git
synced 2026-05-08 17:49:37 +01:00
Add remote start/stop button for supported Subaru vehicles (#167100)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
"""Support for Subaru remote service buttons."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from subarulink import Controller as SubaruAPI
|
||||
|
||||
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import get_device_info
|
||||
from .const import (
|
||||
SERVICE_REMOTE_START,
|
||||
SERVICE_REMOTE_STOP,
|
||||
VEHICLE_HAS_EV,
|
||||
VEHICLE_HAS_REMOTE_START,
|
||||
VEHICLE_VIN,
|
||||
)
|
||||
from .coordinator import SubaruConfigEntry, SubaruDataUpdateCoordinator
|
||||
from .remote_service import async_call_remote_service
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class SubaruButtonEntityDescription(ButtonEntityDescription):
|
||||
"""Describes a Subaru button entity."""
|
||||
|
||||
arg: Callable[[dict[str, Any]], str | None] | None = None
|
||||
|
||||
|
||||
REMOTE_BUTTONS = [
|
||||
SubaruButtonEntityDescription(
|
||||
key=SERVICE_REMOTE_START,
|
||||
translation_key="remote_start",
|
||||
arg=lambda _: "Auto",
|
||||
),
|
||||
SubaruButtonEntityDescription(
|
||||
key=SERVICE_REMOTE_STOP,
|
||||
translation_key="remote_stop",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: SubaruConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Subaru remote service buttons by config_entry."""
|
||||
coordinator = config_entry.runtime_data.coordinator
|
||||
controller = config_entry.runtime_data.controller
|
||||
vehicle_info = config_entry.runtime_data.vehicles
|
||||
async_add_entities(
|
||||
SubaruButton(vehicle, controller, coordinator, description)
|
||||
for vehicle in vehicle_info.values()
|
||||
if vehicle[VEHICLE_HAS_REMOTE_START] or vehicle[VEHICLE_HAS_EV]
|
||||
for description in REMOTE_BUTTONS
|
||||
)
|
||||
|
||||
|
||||
class SubaruButton(ButtonEntity):
|
||||
"""Class for a Subaru button."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
entity_description: SubaruButtonEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
vehicle_info: dict[str, Any],
|
||||
controller: SubaruAPI,
|
||||
coordinator: SubaruDataUpdateCoordinator,
|
||||
description: SubaruButtonEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the button for the vehicle."""
|
||||
self.controller = controller
|
||||
self.coordinator = coordinator
|
||||
self.vehicle_info = vehicle_info
|
||||
self.entity_description = description
|
||||
vin = vehicle_info[VEHICLE_VIN]
|
||||
self._attr_unique_id = f"{vin}_{description.key}"
|
||||
self._attr_device_info = get_device_info(vehicle_info)
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Press the button."""
|
||||
arg = (
|
||||
self.entity_description.arg(self.vehicle_info)
|
||||
if self.entity_description.arg
|
||||
else None
|
||||
)
|
||||
await async_call_remote_service(
|
||||
self.controller,
|
||||
self.entity_description.key,
|
||||
self.vehicle_info,
|
||||
arg,
|
||||
)
|
||||
await self.coordinator.async_refresh()
|
||||
@@ -32,12 +32,15 @@ API_GEN_3 = "g3"
|
||||
MANUFACTURER = "Subaru"
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.BUTTON,
|
||||
Platform.DEVICE_TRACKER,
|
||||
Platform.LOCK,
|
||||
Platform.SENSOR,
|
||||
]
|
||||
|
||||
SERVICE_LOCK = "lock"
|
||||
SERVICE_REMOTE_START = "remote_start"
|
||||
SERVICE_REMOTE_STOP = "remote_stop"
|
||||
SERVICE_UNLOCK = "unlock"
|
||||
SERVICE_UNLOCK_SPECIFIC_DOOR = "unlock_specific_door"
|
||||
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
{
|
||||
"entity": {
|
||||
"button": {
|
||||
"remote_start": {
|
||||
"default": "mdi:power"
|
||||
},
|
||||
"remote_stop": {
|
||||
"default": "mdi:stop-circle-outline"
|
||||
}
|
||||
},
|
||||
"device_tracker": {
|
||||
"location": {
|
||||
"default": "mdi:car"
|
||||
|
||||
@@ -6,7 +6,7 @@ from subarulink.exceptions import SubaruException
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from .const import SERVICE_UNLOCK, VEHICLE_NAME, VEHICLE_VIN
|
||||
from .const import SERVICE_REMOTE_START, SERVICE_UNLOCK, VEHICLE_NAME, VEHICLE_VIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -20,7 +20,7 @@ async def async_call_remote_service(controller, cmd, vehicle_info, arg=None):
|
||||
success = False
|
||||
err_msg = ""
|
||||
try:
|
||||
if cmd == SERVICE_UNLOCK:
|
||||
if cmd in (SERVICE_UNLOCK, SERVICE_REMOTE_START):
|
||||
success = await getattr(controller, cmd)(vin, arg)
|
||||
else:
|
||||
success = await getattr(controller, cmd)(vin)
|
||||
|
||||
@@ -47,6 +47,14 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"button": {
|
||||
"remote_start": {
|
||||
"name": "Remote start"
|
||||
},
|
||||
"remote_stop": {
|
||||
"name": "Remote stop"
|
||||
}
|
||||
},
|
||||
"lock": {
|
||||
"door_locks": {
|
||||
"name": "Door locks"
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
"""Test Subaru buttons."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from subarulink import SubaruException
|
||||
|
||||
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN
|
||||
from homeassistant.components.subaru.const import (
|
||||
VEHICLE_HAS_EV,
|
||||
VEHICLE_HAS_REMOTE_START,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .api_responses import (
|
||||
TEST_VIN_2_EV,
|
||||
TEST_VIN_3_G3,
|
||||
VEHICLE_DATA,
|
||||
VEHICLE_STATUS_EV,
|
||||
VEHICLE_STATUS_G3,
|
||||
)
|
||||
from .conftest import (
|
||||
MOCK_API,
|
||||
MOCK_API_FETCH,
|
||||
MOCK_API_GET_DATA,
|
||||
setup_subaru_config_entry,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
MOCK_API_REMOTE_START = f"{MOCK_API}remote_start"
|
||||
MOCK_API_REMOTE_STOP = f"{MOCK_API}remote_stop"
|
||||
|
||||
VEHICLE_BUTTONS = {
|
||||
TEST_VIN_2_EV: {
|
||||
"remote_start": "button.test_vehicle_2_remote_start",
|
||||
"remote_stop": "button.test_vehicle_2_remote_stop",
|
||||
},
|
||||
TEST_VIN_3_G3: {
|
||||
"remote_start": "button.test_vehicle_3_remote_start",
|
||||
"remote_stop": "button.test_vehicle_3_remote_stop",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("vin", [TEST_VIN_2_EV, TEST_VIN_3_G3])
|
||||
async def test_device_exists(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
subaru_config_entry: MockConfigEntry,
|
||||
vin: str,
|
||||
) -> None:
|
||||
"""Test subaru remote button entities exist."""
|
||||
await setup_subaru_config_entry(
|
||||
hass,
|
||||
subaru_config_entry,
|
||||
vehicle_list=[vin],
|
||||
vehicle_data=VEHICLE_DATA[vin],
|
||||
)
|
||||
entry = entity_registry.async_get(VEHICLE_BUTTONS[vin]["remote_start"])
|
||||
assert entry
|
||||
entry = entity_registry.async_get(VEHICLE_BUTTONS[vin]["remote_stop"])
|
||||
assert entry
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("vin", "vehicle_status"),
|
||||
[
|
||||
(TEST_VIN_2_EV, VEHICLE_STATUS_EV),
|
||||
(TEST_VIN_3_G3, VEHICLE_STATUS_G3),
|
||||
],
|
||||
)
|
||||
async def test_remote_start(
|
||||
hass: HomeAssistant,
|
||||
subaru_config_entry: MockConfigEntry,
|
||||
vin: str,
|
||||
vehicle_status: dict,
|
||||
) -> None:
|
||||
"""Test subaru remote start button."""
|
||||
await setup_subaru_config_entry(
|
||||
hass,
|
||||
subaru_config_entry,
|
||||
vehicle_list=[vin],
|
||||
vehicle_data=VEHICLE_DATA[vin],
|
||||
)
|
||||
with (
|
||||
patch(MOCK_API_REMOTE_START) as mock_remote_start,
|
||||
patch(MOCK_API_FETCH),
|
||||
patch(MOCK_API_GET_DATA, return_value=vehicle_status),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
BUTTON_DOMAIN,
|
||||
"press",
|
||||
{ATTR_ENTITY_ID: VEHICLE_BUTTONS[vin]["remote_start"]},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
mock_remote_start.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("vin", "vehicle_status"),
|
||||
[
|
||||
(TEST_VIN_2_EV, VEHICLE_STATUS_EV),
|
||||
(TEST_VIN_3_G3, VEHICLE_STATUS_G3),
|
||||
],
|
||||
)
|
||||
async def test_remote_stop(
|
||||
hass: HomeAssistant,
|
||||
subaru_config_entry: MockConfigEntry,
|
||||
vin: str,
|
||||
vehicle_status: dict,
|
||||
) -> None:
|
||||
"""Test subaru remote stop button."""
|
||||
await setup_subaru_config_entry(
|
||||
hass,
|
||||
subaru_config_entry,
|
||||
vehicle_list=[vin],
|
||||
vehicle_data=VEHICLE_DATA[vin],
|
||||
)
|
||||
with (
|
||||
patch(MOCK_API_REMOTE_STOP) as mock_remote_stop,
|
||||
patch(MOCK_API_FETCH),
|
||||
patch(MOCK_API_GET_DATA, return_value=vehicle_status),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
BUTTON_DOMAIN,
|
||||
"press",
|
||||
{ATTR_ENTITY_ID: VEHICLE_BUTTONS[vin]["remote_stop"]},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
mock_remote_stop.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("vin", "vehicle_status"),
|
||||
[
|
||||
(TEST_VIN_2_EV, VEHICLE_STATUS_EV),
|
||||
(TEST_VIN_3_G3, VEHICLE_STATUS_G3),
|
||||
],
|
||||
)
|
||||
async def test_remote_start_fails(
|
||||
hass: HomeAssistant,
|
||||
subaru_config_entry: MockConfigEntry,
|
||||
vin: str,
|
||||
vehicle_status: dict,
|
||||
) -> None:
|
||||
"""Test subaru remote start button failure."""
|
||||
await setup_subaru_config_entry(
|
||||
hass,
|
||||
subaru_config_entry,
|
||||
vehicle_list=[vin],
|
||||
vehicle_data=VEHICLE_DATA[vin],
|
||||
)
|
||||
with (
|
||||
patch(MOCK_API_REMOTE_START, return_value=False),
|
||||
patch(MOCK_API_FETCH),
|
||||
patch(MOCK_API_GET_DATA, return_value=vehicle_status),
|
||||
pytest.raises(HomeAssistantError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
BUTTON_DOMAIN,
|
||||
"press",
|
||||
{ATTR_ENTITY_ID: VEHICLE_BUTTONS[vin]["remote_start"]},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("vin", "vehicle_status"),
|
||||
[
|
||||
(TEST_VIN_2_EV, VEHICLE_STATUS_EV),
|
||||
(TEST_VIN_3_G3, VEHICLE_STATUS_G3),
|
||||
],
|
||||
)
|
||||
async def test_remote_start_exception(
|
||||
hass: HomeAssistant,
|
||||
subaru_config_entry: MockConfigEntry,
|
||||
vin: str,
|
||||
vehicle_status: dict,
|
||||
) -> None:
|
||||
"""Test subaru remote start button with SubaruException."""
|
||||
await setup_subaru_config_entry(
|
||||
hass,
|
||||
subaru_config_entry,
|
||||
vehicle_list=[vin],
|
||||
vehicle_data=VEHICLE_DATA[vin],
|
||||
)
|
||||
with (
|
||||
patch(
|
||||
MOCK_API_REMOTE_START,
|
||||
side_effect=SubaruException("Remote service failed"),
|
||||
),
|
||||
patch(MOCK_API_FETCH),
|
||||
patch(MOCK_API_GET_DATA, return_value=vehicle_status),
|
||||
pytest.raises(HomeAssistantError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
BUTTON_DOMAIN,
|
||||
"press",
|
||||
{ATTR_ENTITY_ID: VEHICLE_BUTTONS[vin]["remote_start"]},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("vin", "vehicle_status"),
|
||||
[
|
||||
(TEST_VIN_2_EV, VEHICLE_STATUS_EV),
|
||||
(TEST_VIN_3_G3, VEHICLE_STATUS_G3),
|
||||
],
|
||||
)
|
||||
async def test_remote_stop_fails(
|
||||
hass: HomeAssistant,
|
||||
subaru_config_entry: MockConfigEntry,
|
||||
vin: str,
|
||||
vehicle_status: dict,
|
||||
) -> None:
|
||||
"""Test subaru remote stop button failure."""
|
||||
await setup_subaru_config_entry(
|
||||
hass,
|
||||
subaru_config_entry,
|
||||
vehicle_list=[vin],
|
||||
vehicle_data=VEHICLE_DATA[vin],
|
||||
)
|
||||
with (
|
||||
patch(MOCK_API_REMOTE_STOP, return_value=False),
|
||||
patch(MOCK_API_FETCH),
|
||||
patch(MOCK_API_GET_DATA, return_value=vehicle_status),
|
||||
pytest.raises(HomeAssistantError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
BUTTON_DOMAIN,
|
||||
"press",
|
||||
{ATTR_ENTITY_ID: VEHICLE_BUTTONS[vin]["remote_stop"]},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("vin", "vehicle_status"),
|
||||
[
|
||||
(TEST_VIN_2_EV, VEHICLE_STATUS_EV),
|
||||
(TEST_VIN_3_G3, VEHICLE_STATUS_G3),
|
||||
],
|
||||
)
|
||||
async def test_remote_stop_exception(
|
||||
hass: HomeAssistant,
|
||||
subaru_config_entry: MockConfigEntry,
|
||||
vin: str,
|
||||
vehicle_status: dict,
|
||||
) -> None:
|
||||
"""Test subaru remote stop button with SubaruException."""
|
||||
await setup_subaru_config_entry(
|
||||
hass,
|
||||
subaru_config_entry,
|
||||
vehicle_list=[vin],
|
||||
vehicle_data=VEHICLE_DATA[vin],
|
||||
)
|
||||
with (
|
||||
patch(
|
||||
MOCK_API_REMOTE_STOP,
|
||||
side_effect=SubaruException("Remote service failed"),
|
||||
),
|
||||
patch(MOCK_API_FETCH),
|
||||
patch(MOCK_API_GET_DATA, return_value=vehicle_status),
|
||||
pytest.raises(HomeAssistantError),
|
||||
):
|
||||
await hass.services.async_call(
|
||||
BUTTON_DOMAIN,
|
||||
"press",
|
||||
{ATTR_ENTITY_ID: VEHICLE_BUTTONS[vin]["remote_stop"]},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_no_buttons_without_remote_start(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
subaru_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test no buttons created for vehicle without remote start or EV."""
|
||||
vehicle_data = {
|
||||
**VEHICLE_DATA[TEST_VIN_3_G3],
|
||||
VEHICLE_HAS_REMOTE_START: False,
|
||||
VEHICLE_HAS_EV: False,
|
||||
}
|
||||
await setup_subaru_config_entry(
|
||||
hass,
|
||||
subaru_config_entry,
|
||||
vehicle_list=[TEST_VIN_3_G3],
|
||||
vehicle_data=vehicle_data,
|
||||
)
|
||||
entry = entity_registry.async_get(VEHICLE_BUTTONS[TEST_VIN_3_G3]["remote_start"])
|
||||
assert entry is None
|
||||
entry = entity_registry.async_get(VEHICLE_BUTTONS[TEST_VIN_3_G3]["remote_stop"])
|
||||
assert entry is None
|
||||
Reference in New Issue
Block a user