Re-add "deactivate air conditioning" button to bmw_connected_drive (#94765)

This commit is contained in:
Richard Kroegel 2023-06-27 19:55:46 +02:00 committed by GitHub
parent 2092bd225d
commit f1a54a510c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 251 additions and 2 deletions

View File

@ -124,7 +124,6 @@ omit =
homeassistant/components/bluetooth_tracker/* homeassistant/components/bluetooth_tracker/*
homeassistant/components/bmw_connected_drive/__init__.py homeassistant/components/bmw_connected_drive/__init__.py
homeassistant/components/bmw_connected_drive/binary_sensor.py homeassistant/components/bmw_connected_drive/binary_sensor.py
homeassistant/components/bmw_connected_drive/button.py
homeassistant/components/bmw_connected_drive/coordinator.py homeassistant/components/bmw_connected_drive/coordinator.py
homeassistant/components/bmw_connected_drive/lock.py homeassistant/components/bmw_connected_drive/lock.py
homeassistant/components/bmw_connected_drive/notify.py homeassistant/components/bmw_connected_drive/notify.py

View File

@ -34,6 +34,7 @@ class BMWButtonEntityDescription(ButtonEntityDescription):
[MyBMWVehicle], Coroutine[Any, Any, RemoteServiceStatus] [MyBMWVehicle], Coroutine[Any, Any, RemoteServiceStatus]
] | None = None ] | None = None
account_function: Callable[[BMWDataUpdateCoordinator], Coroutine] | None = None account_function: Callable[[BMWDataUpdateCoordinator], Coroutine] | None = None
is_available: Callable[[MyBMWVehicle], bool] = lambda _: True
BUTTON_TYPES: tuple[BMWButtonEntityDescription, ...] = ( BUTTON_TYPES: tuple[BMWButtonEntityDescription, ...] = (
@ -55,6 +56,13 @@ BUTTON_TYPES: tuple[BMWButtonEntityDescription, ...] = (
icon="mdi:hvac", icon="mdi:hvac",
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(),
is_available=lambda vehicle: vehicle.is_remote_climate_stop_enabled,
),
BMWButtonEntityDescription( BMWButtonEntityDescription(
key="find_vehicle", key="find_vehicle",
translation_key="find_vehicle", translation_key="find_vehicle",
@ -86,7 +94,7 @@ async def async_setup_entry(
[ [
BMWButton(coordinator, vehicle, description) BMWButton(coordinator, vehicle, description)
for description in BUTTON_TYPES for description in BUTTON_TYPES
if not coordinator.read_only if (not coordinator.read_only and description.is_available(vehicle))
or (coordinator.read_only and description.enabled_when_read_only) or (coordinator.read_only and description.enabled_when_read_only)
] ]
) )

View File

@ -1,9 +1,11 @@
"""Tests for the for the BMW Connected Drive integration.""" """Tests for the for the BMW Connected Drive integration."""
from pathlib import Path from pathlib import Path
from urllib.parse import urlparse
from bimmer_connected.api.authentication import MyBMWAuthentication from bimmer_connected.api.authentication import MyBMWAuthentication
from bimmer_connected.const import ( from bimmer_connected.const import (
REMOTE_SERVICE_POSITION_URL,
VEHICLE_CHARGING_DETAILS_URL, VEHICLE_CHARGING_DETAILS_URL,
VEHICLE_STATE_URL, VEHICLE_STATE_URL,
VEHICLES_URL, VEHICLES_URL,
@ -115,6 +117,18 @@ def mock_vehicles() -> respx.Router:
router.get(VEHICLE_CHARGING_DETAILS_URL).mock( router.get(VEHICLE_CHARGING_DETAILS_URL).mock(
side_effect=vehicle_charging_sideeffect side_effect=vehicle_charging_sideeffect
) )
# Get vehicle position after remote service
router.post(urlparse(REMOTE_SERVICE_POSITION_URL).netloc).mock(
httpx.Response(
200,
json=load_json_object_fixture(
FIXTURE_PATH / "remote_service" / "eventposition.json",
integration=BMW_DOMAIN,
),
)
)
return router return router

View File

@ -0,0 +1,12 @@
{
"positionData": {
"status": "OK",
"position": {
"latitude": 123.456,
"longitude": 34.5678,
"formattedAddress": "some_formatted_address",
"heading": 121
}
},
"errorDetails": null
}

View File

@ -0,0 +1,137 @@
# serializer version: 1
# name: test_entity_state_attrs
list([
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by MyBMW',
'friendly_name': 'i4 eDrive40 Flash lights',
'icon': 'mdi:car-light-alert',
}),
'context': <ANY>,
'entity_id': 'button.i4_edrive40_flash_lights',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
}),
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by MyBMW',
'friendly_name': 'i4 eDrive40 Sound horn',
'icon': 'mdi:bullhorn',
}),
'context': <ANY>,
'entity_id': 'button.i4_edrive40_sound_horn',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
}),
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by MyBMW',
'friendly_name': 'i4 eDrive40 Activate air conditioning',
'icon': 'mdi:hvac',
}),
'context': <ANY>,
'entity_id': 'button.i4_edrive40_activate_air_conditioning',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
}),
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by MyBMW',
'friendly_name': 'i4 eDrive40 Deactivate air conditioning',
'icon': 'mdi:hvac-off',
}),
'context': <ANY>,
'entity_id': 'button.i4_edrive40_deactivate_air_conditioning',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
}),
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by MyBMW',
'friendly_name': 'i4 eDrive40 Find vehicle',
'icon': 'mdi:crosshairs-question',
}),
'context': <ANY>,
'entity_id': 'button.i4_edrive40_find_vehicle',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
}),
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by MyBMW',
'friendly_name': 'i4 eDrive40 Refresh from cloud',
'icon': 'mdi:refresh',
}),
'context': <ANY>,
'entity_id': 'button.i4_edrive40_refresh_from_cloud',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
}),
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by MyBMW',
'friendly_name': 'i3 (+ REX) Flash lights',
'icon': 'mdi:car-light-alert',
}),
'context': <ANY>,
'entity_id': 'button.i3_rex_flash_lights',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
}),
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by MyBMW',
'friendly_name': 'i3 (+ REX) Sound horn',
'icon': 'mdi:bullhorn',
}),
'context': <ANY>,
'entity_id': 'button.i3_rex_sound_horn',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
}),
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by MyBMW',
'friendly_name': 'i3 (+ REX) Activate air conditioning',
'icon': 'mdi:hvac',
}),
'context': <ANY>,
'entity_id': 'button.i3_rex_activate_air_conditioning',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
}),
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by MyBMW',
'friendly_name': 'i3 (+ REX) Find vehicle',
'icon': 'mdi:crosshairs-question',
}),
'context': <ANY>,
'entity_id': 'button.i3_rex_find_vehicle',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
}),
StateSnapshot({
'attributes': ReadOnlyDict({
'attribution': 'Data provided by MyBMW',
'friendly_name': 'i3 (+ REX) Refresh from cloud',
'icon': 'mdi:refresh',
}),
'context': <ANY>,
'entity_id': 'button.i3_rex_refresh_from_cloud',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
}),
])
# ---

View File

@ -0,0 +1,79 @@
"""Test BMW buttons."""
from bimmer_connected.vehicle.remote_services import RemoteServices
import pytest
import respx
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.bmw_connected_drive.coordinator import (
BMWDataUpdateCoordinator,
)
from homeassistant.core import HomeAssistant
from . import setup_mocked_integration
async def test_entity_state_attrs(
hass: HomeAssistant,
bmw_fixture: respx.Router,
snapshot: SnapshotAssertion,
) -> None:
"""Test button options and values."""
# Setup component
assert await setup_mocked_integration(hass)
# Get all button entities
assert hass.states.async_all("button") == snapshot
@pytest.mark.parametrize(
("entity_id"),
[
("button.i4_edrive40_flash_lights"),
("button.i4_edrive40_sound_horn"),
("button.i4_edrive40_activate_air_conditioning"),
("button.i4_edrive40_deactivate_air_conditioning"),
("button.i4_edrive40_find_vehicle"),
],
)
async def test_update_triggers_success(
hass: HomeAssistant,
entity_id: str,
bmw_fixture: respx.Router,
) -> None:
"""Test button press."""
# Setup component
assert await setup_mocked_integration(hass)
BMWDataUpdateCoordinator.async_update_listeners.reset_mock()
# Test
await hass.services.async_call(
"button",
"press",
blocking=True,
target={"entity_id": entity_id},
)
assert RemoteServices.trigger_remote_service.call_count == 1
assert BMWDataUpdateCoordinator.async_update_listeners.call_count == 1
async def test_refresh_from_cloud(
hass: HomeAssistant,
bmw_fixture: respx.Router,
) -> None:
"""Test button press for deprecated service."""
# Setup component
assert await setup_mocked_integration(hass)
BMWDataUpdateCoordinator.async_update_listeners.reset_mock()
# Test
await hass.services.async_call(
"button",
"press",
blocking=True,
target={"entity_id": "button.i4_edrive40_refresh_from_cloud"},
)
assert RemoteServices.trigger_remote_service.call_count == 0
assert BMWDataUpdateCoordinator.async_update_listeners.call_count == 2