mirror of
https://github.com/home-assistant/core.git
synced 2025-07-26 06:37:52 +00:00
Add climate on/off for supported BMW vehicles (#92962)
* Add switch platform * Add tests * Remove separate button * Bump coverage --------- Co-authored-by: rikroe <rikroe@users.noreply.github.com>
This commit is contained in:
parent
28fa6f541f
commit
f8f83906f8
@ -44,6 +44,7 @@ PLATFORMS = [
|
|||||||
Platform.NUMBER,
|
Platform.NUMBER,
|
||||||
Platform.SELECT,
|
Platform.SELECT,
|
||||||
Platform.SENSOR,
|
Platform.SENSOR,
|
||||||
|
Platform.SWITCH,
|
||||||
]
|
]
|
||||||
|
|
||||||
SERVICE_UPDATE_STATE = "update_state"
|
SERVICE_UPDATE_STATE = "update_state"
|
||||||
|
@ -53,12 +53,6 @@ BUTTON_TYPES: tuple[BMWButtonEntityDescription, ...] = (
|
|||||||
name="Activate air conditioning",
|
name="Activate air conditioning",
|
||||||
remote_function=lambda vehicle: vehicle.remote_services.trigger_remote_air_conditioning(),
|
remote_function=lambda vehicle: vehicle.remote_services.trigger_remote_air_conditioning(),
|
||||||
),
|
),
|
||||||
BMWButtonEntityDescription(
|
|
||||||
key="deactivate_air_conditioning",
|
|
||||||
icon="mdi:hvac-off",
|
|
||||||
name="Deactivate air conditioning",
|
|
||||||
remote_function=lambda vehicle: vehicle.remote_services.trigger_remote_air_conditioning_stop(),
|
|
||||||
),
|
|
||||||
BMWButtonEntityDescription(
|
BMWButtonEntityDescription(
|
||||||
key="find_vehicle",
|
key="find_vehicle",
|
||||||
icon="mdi:crosshairs-question",
|
icon="mdi:crosshairs-question",
|
||||||
|
109
homeassistant/components/bmw_connected_drive/switch.py
Normal file
109
homeassistant/components/bmw_connected_drive/switch.py
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
"""Switch platform for BMW."""
|
||||||
|
|
||||||
|
from collections.abc import Callable, Coroutine
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from bimmer_connected.models import MyBMWAPIError
|
||||||
|
from bimmer_connected.vehicle import MyBMWVehicle
|
||||||
|
|
||||||
|
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
|
from . import BMWBaseEntity
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .coordinator import BMWDataUpdateCoordinator
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BMWRequiredKeysMixin:
|
||||||
|
"""Mixin for required keys."""
|
||||||
|
|
||||||
|
value_fn: Callable[[MyBMWVehicle], bool]
|
||||||
|
remote_service_on: Callable[[MyBMWVehicle], Coroutine[Any, Any, Any]]
|
||||||
|
remote_service_off: Callable[[MyBMWVehicle], Coroutine[Any, Any, Any]]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BMWSwitchEntityDescription(SwitchEntityDescription, BMWRequiredKeysMixin):
|
||||||
|
"""Describes BMW switch entity."""
|
||||||
|
|
||||||
|
is_available: Callable[[MyBMWVehicle], bool] = lambda _: False
|
||||||
|
dynamic_options: Callable[[MyBMWVehicle], list[str]] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
NUMBER_TYPES: list[BMWSwitchEntityDescription] = [
|
||||||
|
BMWSwitchEntityDescription(
|
||||||
|
key="climate",
|
||||||
|
name="Climate",
|
||||||
|
is_available=lambda v: v.is_remote_climate_stop_enabled,
|
||||||
|
value_fn=lambda v: v.climate.is_climate_on,
|
||||||
|
remote_service_on=lambda v: v.remote_services.trigger_remote_air_conditioning(),
|
||||||
|
remote_service_off=lambda v: v.remote_services.trigger_remote_air_conditioning_stop(),
|
||||||
|
icon="mdi:fan",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up the MyBMW switch from config entry."""
|
||||||
|
coordinator: BMWDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
|
|
||||||
|
entities: list[BMWSwitch] = []
|
||||||
|
|
||||||
|
for vehicle in coordinator.account.vehicles:
|
||||||
|
if not coordinator.read_only:
|
||||||
|
entities.extend(
|
||||||
|
[
|
||||||
|
BMWSwitch(coordinator, vehicle, description)
|
||||||
|
for description in NUMBER_TYPES
|
||||||
|
if description.is_available(vehicle)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
|
class BMWSwitch(BMWBaseEntity, SwitchEntity):
|
||||||
|
"""Representation of BMW Switch entity."""
|
||||||
|
|
||||||
|
entity_description: BMWSwitchEntityDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: BMWDataUpdateCoordinator,
|
||||||
|
vehicle: MyBMWVehicle,
|
||||||
|
description: BMWSwitchEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize an BMW Switch."""
|
||||||
|
super().__init__(coordinator, vehicle)
|
||||||
|
self.entity_description = description
|
||||||
|
self._attr_unique_id = f"{vehicle.vin}-{description.key}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool:
|
||||||
|
"""Return the entity value to represent the entity state."""
|
||||||
|
return self.entity_description.value_fn(self.vehicle)
|
||||||
|
|
||||||
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn the switch on."""
|
||||||
|
try:
|
||||||
|
await self.entity_description.remote_service_on(self.vehicle)
|
||||||
|
except MyBMWAPIError as ex:
|
||||||
|
raise HomeAssistantError(ex) from ex
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn the switch off."""
|
||||||
|
try:
|
||||||
|
await self.entity_description.remote_service_off(self.vehicle)
|
||||||
|
except MyBMWAPIError as ex:
|
||||||
|
raise HomeAssistantError(ex) from ex
|
@ -0,0 +1,17 @@
|
|||||||
|
# serializer version: 1
|
||||||
|
# name: test_entity_state_attrs
|
||||||
|
list([
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'attribution': 'Data provided by MyBMW',
|
||||||
|
'friendly_name': 'i4 eDrive40 Climate',
|
||||||
|
'icon': 'mdi:fan',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'switch.i4_edrive40_climate',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'off',
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
# ---
|
100
tests/components/bmw_connected_drive/test_switch.py
Normal file
100
tests/components/bmw_connected_drive/test_switch.py
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
"""Test BMW switches."""
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
from bimmer_connected.models import MyBMWAPIError, MyBMWRemoteServiceError
|
||||||
|
from bimmer_connected.vehicle.remote_services import RemoteServices
|
||||||
|
import pytest
|
||||||
|
import respx
|
||||||
|
from syrupy.assertion import SnapshotAssertion
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
|
||||||
|
from . import setup_mocked_integration
|
||||||
|
|
||||||
|
|
||||||
|
async def test_entity_state_attrs(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
bmw_fixture: respx.Router,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
) -> None:
|
||||||
|
"""Test switch options and values.."""
|
||||||
|
|
||||||
|
# Setup component
|
||||||
|
assert await setup_mocked_integration(hass)
|
||||||
|
|
||||||
|
# Get all switch entities
|
||||||
|
assert hass.states.async_all("switch") == snapshot
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("entity_id", "value"),
|
||||||
|
[
|
||||||
|
("switch.i4_edrive40_climate", "ON"),
|
||||||
|
("switch.i4_edrive40_climate", "OFF"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_update_triggers_success(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity_id: str,
|
||||||
|
value: str,
|
||||||
|
bmw_fixture: respx.Router,
|
||||||
|
) -> None:
|
||||||
|
"""Test allowed values for switch inputs."""
|
||||||
|
|
||||||
|
# Setup component
|
||||||
|
assert await setup_mocked_integration(hass)
|
||||||
|
|
||||||
|
# Test
|
||||||
|
await hass.services.async_call(
|
||||||
|
"switch",
|
||||||
|
f"turn_{value.lower()}",
|
||||||
|
blocking=True,
|
||||||
|
target={"entity_id": entity_id},
|
||||||
|
)
|
||||||
|
assert RemoteServices.trigger_remote_service.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("raised", "expected"),
|
||||||
|
[
|
||||||
|
(MyBMWRemoteServiceError, HomeAssistantError),
|
||||||
|
(MyBMWAPIError, HomeAssistantError),
|
||||||
|
(ValueError, ValueError),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_update_triggers_exceptions(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
raised: Exception,
|
||||||
|
expected: Exception,
|
||||||
|
bmw_fixture: respx.Router,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
"""Test not allowed values for switch inputs."""
|
||||||
|
|
||||||
|
# Setup component
|
||||||
|
assert await setup_mocked_integration(hass)
|
||||||
|
|
||||||
|
# Setup exception
|
||||||
|
monkeypatch.setattr(
|
||||||
|
RemoteServices,
|
||||||
|
"trigger_remote_service",
|
||||||
|
AsyncMock(side_effect=raised),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test
|
||||||
|
with pytest.raises(expected):
|
||||||
|
await hass.services.async_call(
|
||||||
|
"switch",
|
||||||
|
"turn_on",
|
||||||
|
blocking=True,
|
||||||
|
target={"entity_id": "switch.i4_edrive40_climate"},
|
||||||
|
)
|
||||||
|
with pytest.raises(expected):
|
||||||
|
await hass.services.async_call(
|
||||||
|
"switch",
|
||||||
|
"turn_off",
|
||||||
|
blocking=True,
|
||||||
|
target={"entity_id": "switch.i4_edrive40_climate"},
|
||||||
|
)
|
||||||
|
assert RemoteServices.trigger_remote_service.call_count == 2
|
Loading…
x
Reference in New Issue
Block a user