From 9dc2b99c20440c9344a560a16fb092530cd7bb93 Mon Sep 17 00:00:00 2001 From: Thomas D <11554546+thomasddn@users.noreply.github.com> Date: Fri, 10 Oct 2025 14:50:28 +0000 Subject: [PATCH 1/6] Add lock --- homeassistant/components/volvo/const.py | 1 + homeassistant/components/volvo/lock.py | 150 +++++++ homeassistant/components/volvo/strings.json | 11 + .../components/volvo/snapshots/test_lock.ambr | 393 ++++++++++++++++++ tests/components/volvo/test_lock.py | 136 ++++++ 5 files changed, 691 insertions(+) create mode 100644 homeassistant/components/volvo/lock.py create mode 100644 tests/components/volvo/snapshots/test_lock.ambr create mode 100644 tests/components/volvo/test_lock.py diff --git a/homeassistant/components/volvo/const.py b/homeassistant/components/volvo/const.py index 82eb87374f5fb..3c5e1d74f4977 100644 --- a/homeassistant/components/volvo/const.py +++ b/homeassistant/components/volvo/const.py @@ -7,6 +7,7 @@ Platform.BINARY_SENSOR, Platform.BUTTON, Platform.DEVICE_TRACKER, + Platform.LOCK, Platform.SENSOR, ] diff --git a/homeassistant/components/volvo/lock.py b/homeassistant/components/volvo/lock.py new file mode 100644 index 0000000000000..b31cda7c020bd --- /dev/null +++ b/homeassistant/components/volvo/lock.py @@ -0,0 +1,150 @@ +"""Volvo locks.""" + +from dataclasses import dataclass +import logging +from typing import Any, cast + +from volvocarsapi.models import VolvoApiException, VolvoCarsApiBaseModel, VolvoCarsValue + +from homeassistant.components.lock import LockEntity, LockEntityDescription +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .const import DOMAIN +from .coordinator import VolvoConfigEntry +from .entity import VolvoEntity, VolvoEntityDescription + +PARALLEL_UPDATES = 0 +_LOGGER = logging.getLogger(__name__) + + +@dataclass(frozen=True, kw_only=True) +class VolvoLockDescription(VolvoEntityDescription, LockEntityDescription): + """Describes a Volvo lock entity.""" + + api_lock_value: str = "LOCKED" + api_unlock_value: str = "UNLOCKED" + lock_command: str + unlock_command: str + required_command_key: str + + +_DESCRIPTIONS: tuple[VolvoLockDescription, ...] = ( + VolvoLockDescription( + key="lock", + api_field="centralLock", + lock_command="lock", + unlock_command="unlock", + required_command_key="LOCK", + ), + VolvoLockDescription( + key="lock_reduced_guard", + api_field="centralLock", + lock_command="lock-reduced-guard", + unlock_command="unlock", + required_command_key="LOCK_REDUCED_GUARD", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: VolvoConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up locks.""" + coordinators = entry.runtime_data.interval_coordinators + async_add_entities( + [ + VolvoLock(coordinator, description) + for coordinator in coordinators + for description in _DESCRIPTIONS + if description.required_command_key + in entry.runtime_data.context.supported_commands + and description.api_field in coordinator.data + ] + ) + + +class VolvoLock(VolvoEntity, LockEntity): + """Volvo lock.""" + + entity_description: VolvoLockDescription + + async def async_lock(self, **kwargs: Any) -> None: + """Lock the car.""" + await self._async_handle_command(self.entity_description.lock_command, True) + + async def async_unlock(self, **kwargs: Any) -> None: + """Unlock the car.""" + await self._async_handle_command(self.entity_description.unlock_command, False) + + def _update_state(self, api_field: VolvoCarsApiBaseModel | None) -> None: + """Update the state of the entity.""" + assert isinstance(api_field, VolvoCarsValue) + self._attr_is_locked = api_field.value == "LOCKED" + + async def _async_handle_command(self, command: str, locked: bool) -> None: + _LOGGER.debug("Lock '%s' is %s", command, "locked" if locked else "unlocked") + if locked: + self._attr_is_locking = True + else: + self._attr_is_unlocking = True + self.async_write_ha_state() + + try: + result = await self.coordinator.context.api.async_execute_command(command) + except VolvoApiException as ex: + _LOGGER.debug("Lock '%s' error", command) + self._reset_and_raise(command, message=ex.message, exception=ex) + + status = result.invoke_status if result else "" + _LOGGER.debug("Lock '%s' result: %s", command, status) + + if status.upper() not in ("COMPLETED", "DELIVERED"): + self._reset_and_raise( + command, status=status, message=result.message if result else "" + ) + + api_field = cast( + VolvoCarsValue, + self.coordinator.get_api_field(self.entity_description.api_field), + ) + + if locked: + self._attr_is_locking = False + api_field.value = self.entity_description.api_lock_value + else: + self._attr_is_unlocking = False + api_field.value = self.entity_description.api_unlock_value + + self._attr_is_locked = locked + self.async_write_ha_state() + + def _reset_and_raise( + self, + command: str, + *, + status: str = "", + message: str = "", + exception: Exception | None = None, + ) -> None: + self._attr_is_locking = False + self._attr_is_unlocking = False + self.async_write_ha_state() + + error = HomeAssistantError( + translation_domain=DOMAIN, + translation_key="lock_failure", + translation_placeholders={ + "command": command, + "status": status, + "message": message, + }, + ) + + if exception: + raise error from exception + + raise error diff --git a/homeassistant/components/volvo/strings.json b/homeassistant/components/volvo/strings.json index 8cce41a839f59..ccf5b7980f368 100644 --- a/homeassistant/components/volvo/strings.json +++ b/homeassistant/components/volvo/strings.json @@ -204,6 +204,14 @@ "name": "Honk & flash" } }, + "lock": { + "lock": { + "name": "Central lock" + }, + "lock_reduced_guard": { + "name": "Reduced guard lock" + } + }, "sensor": { "availability": { "name": "Car connection", @@ -327,6 +335,9 @@ "command_failure": { "message": "Command {command} failed. Status: {status}. Message: {message}" }, + "lock_failure": { + "message": "Failed to {command} vehicle. Status: {status}. Message: {message}" + }, "no_vehicle": { "message": "Unable to retrieve vehicle details." }, diff --git a/tests/components/volvo/snapshots/test_lock.ambr b/tests/components/volvo/snapshots/test_lock.ambr new file mode 100644 index 0000000000000..cceac22034d5f --- /dev/null +++ b/tests/components/volvo/snapshots/test_lock.ambr @@ -0,0 +1,393 @@ +# serializer version: 1 +# name: test_lock[ex30_2024][lock.volvo_ex30_central_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'lock', + 'entity_category': None, + 'entity_id': 'lock.volvo_ex30_central_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Central lock', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'lock', + 'unique_id': 'yv1abcdefg1234567_lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_lock[ex30_2024][lock.volvo_ex30_central_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Volvo EX30 Central lock', + 'supported_features': , + }), + 'context': , + 'entity_id': 'lock.volvo_ex30_central_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'locked', + }) +# --- +# name: test_lock[ex30_2024][lock.volvo_ex30_reduced_guard_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'lock', + 'entity_category': None, + 'entity_id': 'lock.volvo_ex30_reduced_guard_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Reduced guard lock', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'lock_reduced_guard', + 'unique_id': 'yv1abcdefg1234567_lock_reduced_guard', + 'unit_of_measurement': None, + }) +# --- +# name: test_lock[ex30_2024][lock.volvo_ex30_reduced_guard_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Volvo EX30 Reduced guard lock', + 'supported_features': , + }), + 'context': , + 'entity_id': 'lock.volvo_ex30_reduced_guard_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'locked', + }) +# --- +# name: test_lock[s90_diesel_2018][lock.volvo_s90_central_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'lock', + 'entity_category': None, + 'entity_id': 'lock.volvo_s90_central_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Central lock', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'lock', + 'unique_id': 'yv1abcdefg1234567_lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_lock[s90_diesel_2018][lock.volvo_s90_central_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Volvo S90 Central lock', + 'supported_features': , + }), + 'context': , + 'entity_id': 'lock.volvo_s90_central_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'locked', + }) +# --- +# name: test_lock[s90_diesel_2018][lock.volvo_s90_reduced_guard_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'lock', + 'entity_category': None, + 'entity_id': 'lock.volvo_s90_reduced_guard_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Reduced guard lock', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'lock_reduced_guard', + 'unique_id': 'yv1abcdefg1234567_lock_reduced_guard', + 'unit_of_measurement': None, + }) +# --- +# name: test_lock[s90_diesel_2018][lock.volvo_s90_reduced_guard_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Volvo S90 Reduced guard lock', + 'supported_features': , + }), + 'context': , + 'entity_id': 'lock.volvo_s90_reduced_guard_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'locked', + }) +# --- +# name: test_lock[xc40_electric_2024][lock.volvo_xc40_central_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'lock', + 'entity_category': None, + 'entity_id': 'lock.volvo_xc40_central_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Central lock', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'lock', + 'unique_id': 'yv1abcdefg1234567_lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_lock[xc40_electric_2024][lock.volvo_xc40_central_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Volvo XC40 Central lock', + 'supported_features': , + }), + 'context': , + 'entity_id': 'lock.volvo_xc40_central_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'locked', + }) +# --- +# name: test_lock[xc40_electric_2024][lock.volvo_xc40_reduced_guard_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'lock', + 'entity_category': None, + 'entity_id': 'lock.volvo_xc40_reduced_guard_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Reduced guard lock', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'lock_reduced_guard', + 'unique_id': 'yv1abcdefg1234567_lock_reduced_guard', + 'unit_of_measurement': None, + }) +# --- +# name: test_lock[xc40_electric_2024][lock.volvo_xc40_reduced_guard_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Volvo XC40 Reduced guard lock', + 'supported_features': , + }), + 'context': , + 'entity_id': 'lock.volvo_xc40_reduced_guard_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'locked', + }) +# --- +# name: test_lock[xc90_petrol_2019][lock.volvo_xc90_central_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'lock', + 'entity_category': None, + 'entity_id': 'lock.volvo_xc90_central_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Central lock', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'lock', + 'unique_id': 'yv1abcdefg1234567_lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_lock[xc90_petrol_2019][lock.volvo_xc90_central_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Volvo XC90 Central lock', + 'supported_features': , + }), + 'context': , + 'entity_id': 'lock.volvo_xc90_central_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'locked', + }) +# --- +# name: test_lock[xc90_petrol_2019][lock.volvo_xc90_reduced_guard_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'lock', + 'entity_category': None, + 'entity_id': 'lock.volvo_xc90_reduced_guard_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Reduced guard lock', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'lock_reduced_guard', + 'unique_id': 'yv1abcdefg1234567_lock_reduced_guard', + 'unit_of_measurement': None, + }) +# --- +# name: test_lock[xc90_petrol_2019][lock.volvo_xc90_reduced_guard_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Volvo XC90 Reduced guard lock', + 'supported_features': , + }), + 'context': , + 'entity_id': 'lock.volvo_xc90_reduced_guard_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'locked', + }) +# --- diff --git a/tests/components/volvo/test_lock.py b/tests/components/volvo/test_lock.py new file mode 100644 index 0000000000000..952b1a24aa1f4 --- /dev/null +++ b/tests/components/volvo/test_lock.py @@ -0,0 +1,136 @@ +"""Test Volvo locks.""" + +from collections.abc import Awaitable, Callable +from unittest.mock import patch + +import pytest +from syrupy.assertion import SnapshotAssertion +from volvocarsapi.api import VolvoCarsApi +from volvocarsapi.models import VolvoApiException, VolvoCarsCommandResult + +from homeassistant.components.lock import ( + DOMAIN as LOCK_DOMAIN, + SERVICE_LOCK, + SERVICE_UNLOCK, + LockState, +) +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er + +from . import configure_mock +from .const import DEFAULT_VIN + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.mark.usefixtures("mock_api", "full_model") +@pytest.mark.parametrize( + "full_model", + ["ex30_2024", "s90_diesel_2018", "xc40_electric_2024", "xc90_petrol_2019"], +) +async def test_lock( + hass: HomeAssistant, + setup_integration: Callable[[], Awaitable[bool]], + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, +) -> None: + """Test lock.""" + + with patch("homeassistant.components.volvo.PLATFORMS", [Platform.LOCK]): + assert await setup_integration() + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.usefixtures("full_model") +@pytest.mark.parametrize( + "action", + [SERVICE_UNLOCK, SERVICE_LOCK], +) +async def test_unlock_lock( + hass: HomeAssistant, + setup_integration: Callable[[], Awaitable[bool]], + mock_api: VolvoCarsApi, + action: str, +) -> None: + """Test unlock/lock.""" + + with patch("homeassistant.components.volvo.PLATFORMS", [Platform.LOCK]): + assert await setup_integration() + + await hass.services.async_call( + LOCK_DOMAIN, + action, + {ATTR_ENTITY_ID: "lock.volvo_xc40_central_lock"}, + blocking=True, + ) + await hass.async_block_till_done() + + assert len(mock_api.async_execute_command.mock_calls) == 1 + + +@pytest.mark.usefixtures("full_model") +@pytest.mark.parametrize( + "action", + [SERVICE_UNLOCK, SERVICE_LOCK], +) +async def test_unlock_lock_error( + hass: HomeAssistant, + setup_integration: Callable[[], Awaitable[bool]], + mock_api: VolvoCarsApi, + action: str, +) -> None: + """Test unlock/lock with error response.""" + + with patch("homeassistant.components.volvo.PLATFORMS", [Platform.LOCK]): + assert await setup_integration() + + configure_mock(mock_api.async_execute_command, side_effect=VolvoApiException) + + entity_id = "lock.volvo_xc40_central_lock" + assert hass.states.get(entity_id).state == LockState.LOCKED + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + LOCK_DOMAIN, + action, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == LockState.LOCKED + + +@pytest.mark.usefixtures("full_model") +async def test_unlock_failure( + hass: HomeAssistant, + setup_integration: Callable[[], Awaitable[bool]], + mock_api: VolvoCarsApi, +) -> None: + """Test unlock/lock with error response.""" + + with patch("homeassistant.components.volvo.PLATFORMS", [Platform.LOCK]): + assert await setup_integration() + + configure_mock( + mock_api.async_execute_command, + return_value=VolvoCarsCommandResult( + vin=DEFAULT_VIN, invoke_status="CONNECTION_FAILURE", message="" + ), + ) + + entity_id = "lock.volvo_xc40_central_lock" + assert hass.states.get(entity_id).state == LockState.LOCKED + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_UNLOCK, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == LockState.LOCKED From cad335e9305cef4fbeeeb96d3e9473f10c8607ed Mon Sep 17 00:00:00 2001 From: Thomas D <11554546+thomasddn@users.noreply.github.com> Date: Fri, 10 Oct 2025 15:11:49 +0000 Subject: [PATCH 2/6] Refactor raise --- homeassistant/components/volvo/lock.py | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/volvo/lock.py b/homeassistant/components/volvo/lock.py index b31cda7c020bd..9c278a7d91e4b 100644 --- a/homeassistant/components/volvo/lock.py +++ b/homeassistant/components/volvo/lock.py @@ -97,15 +97,17 @@ async def _async_handle_command(self, command: str, locked: bool) -> None: result = await self.coordinator.context.api.async_execute_command(command) except VolvoApiException as ex: _LOGGER.debug("Lock '%s' error", command) - self._reset_and_raise(command, message=ex.message, exception=ex) + error = self._reset_and_create_error(command, message=ex.message) + raise error from ex status = result.invoke_status if result else "" _LOGGER.debug("Lock '%s' result: %s", command, status) if status.upper() not in ("COMPLETED", "DELIVERED"): - self._reset_and_raise( + error = self._reset_and_create_error( command, status=status, message=result.message if result else "" ) + raise error api_field = cast( VolvoCarsValue, @@ -122,19 +124,14 @@ async def _async_handle_command(self, command: str, locked: bool) -> None: self._attr_is_locked = locked self.async_write_ha_state() - def _reset_and_raise( - self, - command: str, - *, - status: str = "", - message: str = "", - exception: Exception | None = None, - ) -> None: + def _reset_and_create_error( + self, command: str, *, status: str = "", message: str = "" + ) -> HomeAssistantError: self._attr_is_locking = False self._attr_is_unlocking = False self.async_write_ha_state() - error = HomeAssistantError( + return HomeAssistantError( translation_domain=DOMAIN, translation_key="lock_failure", translation_placeholders={ @@ -143,8 +140,3 @@ def _reset_and_raise( "message": message, }, ) - - if exception: - raise error from exception - - raise error From 8c6c707960c2152901640c2a5954fabaded5427b Mon Sep 17 00:00:00 2001 From: Thomas D <11554546+thomasddn@users.noreply.github.com> Date: Thu, 30 Oct 2025 10:01:03 +0000 Subject: [PATCH 3/6] Remove lock with reduced guard --- homeassistant/components/volvo/lock.py | 7 - homeassistant/components/volvo/strings.json | 3 - .../components/volvo/snapshots/test_lock.ambr | 196 ------------------ 3 files changed, 206 deletions(-) diff --git a/homeassistant/components/volvo/lock.py b/homeassistant/components/volvo/lock.py index 9c278a7d91e4b..245cb9660e6d2 100644 --- a/homeassistant/components/volvo/lock.py +++ b/homeassistant/components/volvo/lock.py @@ -38,13 +38,6 @@ class VolvoLockDescription(VolvoEntityDescription, LockEntityDescription): unlock_command="unlock", required_command_key="LOCK", ), - VolvoLockDescription( - key="lock_reduced_guard", - api_field="centralLock", - lock_command="lock-reduced-guard", - unlock_command="unlock", - required_command_key="LOCK_REDUCED_GUARD", - ), ) diff --git a/homeassistant/components/volvo/strings.json b/homeassistant/components/volvo/strings.json index 20d885a8e4983..01a4cac9bc822 100644 --- a/homeassistant/components/volvo/strings.json +++ b/homeassistant/components/volvo/strings.json @@ -213,9 +213,6 @@ "lock": { "lock": { "name": "Central lock" - }, - "lock_reduced_guard": { - "name": "Reduced guard lock" } }, "sensor": { diff --git a/tests/components/volvo/snapshots/test_lock.ambr b/tests/components/volvo/snapshots/test_lock.ambr index cceac22034d5f..640f810e8673b 100644 --- a/tests/components/volvo/snapshots/test_lock.ambr +++ b/tests/components/volvo/snapshots/test_lock.ambr @@ -48,55 +48,6 @@ 'state': 'locked', }) # --- -# name: test_lock[ex30_2024][lock.volvo_ex30_reduced_guard_lock-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'lock', - 'entity_category': None, - 'entity_id': 'lock.volvo_ex30_reduced_guard_lock', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Reduced guard lock', - 'platform': 'volvo', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'lock_reduced_guard', - 'unique_id': 'yv1abcdefg1234567_lock_reduced_guard', - 'unit_of_measurement': None, - }) -# --- -# name: test_lock[ex30_2024][lock.volvo_ex30_reduced_guard_lock-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Volvo EX30 Reduced guard lock', - 'supported_features': , - }), - 'context': , - 'entity_id': 'lock.volvo_ex30_reduced_guard_lock', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'locked', - }) -# --- # name: test_lock[s90_diesel_2018][lock.volvo_s90_central_lock-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -146,55 +97,6 @@ 'state': 'locked', }) # --- -# name: test_lock[s90_diesel_2018][lock.volvo_s90_reduced_guard_lock-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'lock', - 'entity_category': None, - 'entity_id': 'lock.volvo_s90_reduced_guard_lock', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Reduced guard lock', - 'platform': 'volvo', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'lock_reduced_guard', - 'unique_id': 'yv1abcdefg1234567_lock_reduced_guard', - 'unit_of_measurement': None, - }) -# --- -# name: test_lock[s90_diesel_2018][lock.volvo_s90_reduced_guard_lock-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Volvo S90 Reduced guard lock', - 'supported_features': , - }), - 'context': , - 'entity_id': 'lock.volvo_s90_reduced_guard_lock', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'locked', - }) -# --- # name: test_lock[xc40_electric_2024][lock.volvo_xc40_central_lock-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -244,55 +146,6 @@ 'state': 'locked', }) # --- -# name: test_lock[xc40_electric_2024][lock.volvo_xc40_reduced_guard_lock-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'lock', - 'entity_category': None, - 'entity_id': 'lock.volvo_xc40_reduced_guard_lock', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Reduced guard lock', - 'platform': 'volvo', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'lock_reduced_guard', - 'unique_id': 'yv1abcdefg1234567_lock_reduced_guard', - 'unit_of_measurement': None, - }) -# --- -# name: test_lock[xc40_electric_2024][lock.volvo_xc40_reduced_guard_lock-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Volvo XC40 Reduced guard lock', - 'supported_features': , - }), - 'context': , - 'entity_id': 'lock.volvo_xc40_reduced_guard_lock', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'locked', - }) -# --- # name: test_lock[xc90_petrol_2019][lock.volvo_xc90_central_lock-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -342,52 +195,3 @@ 'state': 'locked', }) # --- -# name: test_lock[xc90_petrol_2019][lock.volvo_xc90_reduced_guard_lock-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'config_subentry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'lock', - 'entity_category': None, - 'entity_id': 'lock.volvo_xc90_reduced_guard_lock', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Reduced guard lock', - 'platform': 'volvo', - 'previous_unique_id': None, - 'suggested_object_id': None, - 'supported_features': 0, - 'translation_key': 'lock_reduced_guard', - 'unique_id': 'yv1abcdefg1234567_lock_reduced_guard', - 'unit_of_measurement': None, - }) -# --- -# name: test_lock[xc90_petrol_2019][lock.volvo_xc90_reduced_guard_lock-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Volvo XC90 Reduced guard lock', - 'supported_features': , - }), - 'context': , - 'entity_id': 'lock.volvo_xc90_reduced_guard_lock', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'locked', - }) -# --- From 2f782e219fe60b754b131c99048418bd7c74efa7 Mon Sep 17 00:00:00 2001 From: Thomas D <11554546+thomasddn@users.noreply.github.com> Date: Sat, 1 Nov 2025 13:58:34 +0000 Subject: [PATCH 4/6] Assure resetting un/lock attributes --- homeassistant/components/volvo/lock.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/volvo/lock.py b/homeassistant/components/volvo/lock.py index 245cb9660e6d2..27b1872a71c04 100644 --- a/homeassistant/components/volvo/lock.py +++ b/homeassistant/components/volvo/lock.py @@ -107,11 +107,12 @@ async def _async_handle_command(self, command: str, locked: bool) -> None: self.coordinator.get_api_field(self.entity_description.api_field), ) + self._attr_is_locking = False + self._attr_is_unlocking = False + if locked: - self._attr_is_locking = False api_field.value = self.entity_description.api_lock_value else: - self._attr_is_unlocking = False api_field.value = self.entity_description.api_unlock_value self._attr_is_locked = locked From 0263570b1477809a55e02a94469c41406fa97551 Mon Sep 17 00:00:00 2001 From: Thomas D <11554546+thomasddn@users.noreply.github.com> Date: Sat, 1 Nov 2025 14:00:16 +0000 Subject: [PATCH 5/6] Assert method call parameters --- tests/components/volvo/test_lock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/volvo/test_lock.py b/tests/components/volvo/test_lock.py index 952b1a24aa1f4..218207ec640d1 100644 --- a/tests/components/volvo/test_lock.py +++ b/tests/components/volvo/test_lock.py @@ -69,7 +69,7 @@ async def test_unlock_lock( ) await hass.async_block_till_done() - assert len(mock_api.async_execute_command.mock_calls) == 1 + mock_api.async_execute_command.assert_called_once_with(action) @pytest.mark.usefixtures("full_model") From 1fb833f14cd8314ea8da1c59bdd82056d17e4586 Mon Sep 17 00:00:00 2001 From: Thomas D <11554546+thomasddn@users.noreply.github.com> Date: Sun, 2 Nov 2025 11:24:58 +0000 Subject: [PATCH 6/6] Use common entity name for lock --- homeassistant/components/volvo/strings.json | 2 +- .../components/volvo/snapshots/test_lock.ambr | 48 +++++++++---------- tests/components/volvo/test_lock.py | 6 +-- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/volvo/strings.json b/homeassistant/components/volvo/strings.json index 01a4cac9bc822..e4c4ac7efcf8c 100644 --- a/homeassistant/components/volvo/strings.json +++ b/homeassistant/components/volvo/strings.json @@ -212,7 +212,7 @@ }, "lock": { "lock": { - "name": "Central lock" + "name": "[%key:component::lock::title%]" } }, "sensor": { diff --git a/tests/components/volvo/snapshots/test_lock.ambr b/tests/components/volvo/snapshots/test_lock.ambr index 640f810e8673b..c1cde82920f29 100644 --- a/tests/components/volvo/snapshots/test_lock.ambr +++ b/tests/components/volvo/snapshots/test_lock.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_lock[ex30_2024][lock.volvo_ex30_central_lock-entry] +# name: test_lock[ex30_2024][lock.volvo_ex30_lock-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -12,7 +12,7 @@ 'disabled_by': None, 'domain': 'lock', 'entity_category': None, - 'entity_id': 'lock.volvo_ex30_central_lock', + 'entity_id': 'lock.volvo_ex30_lock', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -24,7 +24,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Central lock', + 'original_name': 'Lock', 'platform': 'volvo', 'previous_unique_id': None, 'suggested_object_id': None, @@ -34,21 +34,21 @@ 'unit_of_measurement': None, }) # --- -# name: test_lock[ex30_2024][lock.volvo_ex30_central_lock-state] +# name: test_lock[ex30_2024][lock.volvo_ex30_lock-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Volvo EX30 Central lock', + 'friendly_name': 'Volvo EX30 Lock', 'supported_features': , }), 'context': , - 'entity_id': 'lock.volvo_ex30_central_lock', + 'entity_id': 'lock.volvo_ex30_lock', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'locked', }) # --- -# name: test_lock[s90_diesel_2018][lock.volvo_s90_central_lock-entry] +# name: test_lock[s90_diesel_2018][lock.volvo_s90_lock-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -61,7 +61,7 @@ 'disabled_by': None, 'domain': 'lock', 'entity_category': None, - 'entity_id': 'lock.volvo_s90_central_lock', + 'entity_id': 'lock.volvo_s90_lock', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -73,7 +73,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Central lock', + 'original_name': 'Lock', 'platform': 'volvo', 'previous_unique_id': None, 'suggested_object_id': None, @@ -83,21 +83,21 @@ 'unit_of_measurement': None, }) # --- -# name: test_lock[s90_diesel_2018][lock.volvo_s90_central_lock-state] +# name: test_lock[s90_diesel_2018][lock.volvo_s90_lock-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Volvo S90 Central lock', + 'friendly_name': 'Volvo S90 Lock', 'supported_features': , }), 'context': , - 'entity_id': 'lock.volvo_s90_central_lock', + 'entity_id': 'lock.volvo_s90_lock', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'locked', }) # --- -# name: test_lock[xc40_electric_2024][lock.volvo_xc40_central_lock-entry] +# name: test_lock[xc40_electric_2024][lock.volvo_xc40_lock-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -110,7 +110,7 @@ 'disabled_by': None, 'domain': 'lock', 'entity_category': None, - 'entity_id': 'lock.volvo_xc40_central_lock', + 'entity_id': 'lock.volvo_xc40_lock', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -122,7 +122,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Central lock', + 'original_name': 'Lock', 'platform': 'volvo', 'previous_unique_id': None, 'suggested_object_id': None, @@ -132,21 +132,21 @@ 'unit_of_measurement': None, }) # --- -# name: test_lock[xc40_electric_2024][lock.volvo_xc40_central_lock-state] +# name: test_lock[xc40_electric_2024][lock.volvo_xc40_lock-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Volvo XC40 Central lock', + 'friendly_name': 'Volvo XC40 Lock', 'supported_features': , }), 'context': , - 'entity_id': 'lock.volvo_xc40_central_lock', + 'entity_id': 'lock.volvo_xc40_lock', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'locked', }) # --- -# name: test_lock[xc90_petrol_2019][lock.volvo_xc90_central_lock-entry] +# name: test_lock[xc90_petrol_2019][lock.volvo_xc90_lock-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -159,7 +159,7 @@ 'disabled_by': None, 'domain': 'lock', 'entity_category': None, - 'entity_id': 'lock.volvo_xc90_central_lock', + 'entity_id': 'lock.volvo_xc90_lock', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -171,7 +171,7 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Central lock', + 'original_name': 'Lock', 'platform': 'volvo', 'previous_unique_id': None, 'suggested_object_id': None, @@ -181,14 +181,14 @@ 'unit_of_measurement': None, }) # --- -# name: test_lock[xc90_petrol_2019][lock.volvo_xc90_central_lock-state] +# name: test_lock[xc90_petrol_2019][lock.volvo_xc90_lock-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Volvo XC90 Central lock', + 'friendly_name': 'Volvo XC90 Lock', 'supported_features': , }), 'context': , - 'entity_id': 'lock.volvo_xc90_central_lock', + 'entity_id': 'lock.volvo_xc90_lock', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/volvo/test_lock.py b/tests/components/volvo/test_lock.py index 218207ec640d1..dfabc82d6547e 100644 --- a/tests/components/volvo/test_lock.py +++ b/tests/components/volvo/test_lock.py @@ -64,7 +64,7 @@ async def test_unlock_lock( await hass.services.async_call( LOCK_DOMAIN, action, - {ATTR_ENTITY_ID: "lock.volvo_xc40_central_lock"}, + {ATTR_ENTITY_ID: "lock.volvo_xc40_lock"}, blocking=True, ) await hass.async_block_till_done() @@ -90,7 +90,7 @@ async def test_unlock_lock_error( configure_mock(mock_api.async_execute_command, side_effect=VolvoApiException) - entity_id = "lock.volvo_xc40_central_lock" + entity_id = "lock.volvo_xc40_lock" assert hass.states.get(entity_id).state == LockState.LOCKED with pytest.raises(HomeAssistantError): @@ -122,7 +122,7 @@ async def test_unlock_failure( ), ) - entity_id = "lock.volvo_xc40_central_lock" + entity_id = "lock.volvo_xc40_lock" assert hass.states.get(entity_id).state == LockState.LOCKED with pytest.raises(HomeAssistantError):