1
0
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:
Andres Ruiz
2026-04-11 10:22:19 -04:00
committed by GitHub
parent 483265a707
commit 8a43d1a12c
6 changed files with 421 additions and 2 deletions
+99
View File
@@ -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()
+3
View File
@@ -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"
+301
View File
@@ -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