Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions homeassistant/components/volvo/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.DEVICE_TRACKER,
Platform.LOCK,
Platform.SENSOR,
]

Expand Down
136 changes: 136 additions & 0 deletions homeassistant/components/volvo/lock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"""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",
),
)


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)
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"):
error = self._reset_and_create_error(
command, status=status, message=result.message if result else ""
)
raise error

api_field = cast(
VolvoCarsValue,
self.coordinator.get_api_field(self.entity_description.api_field),
)

self._attr_is_locking = False
self._attr_is_unlocking = False

if locked:
api_field.value = self.entity_description.api_lock_value
else:
api_field.value = self.entity_description.api_unlock_value

self._attr_is_locked = locked
self.async_write_ha_state()

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()

return HomeAssistantError(
translation_domain=DOMAIN,
translation_key="lock_failure",
translation_placeholders={
"command": command,
"status": status,
"message": message,
},
)
8 changes: 8 additions & 0 deletions homeassistant/components/volvo/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,11 @@
"name": "Honk & flash"
}
},
"lock": {
"lock": {
"name": "[%key:component::lock::title%]"
}
},
"sensor": {
"availability": {
"name": "Car connection",
Expand Down Expand Up @@ -349,6 +354,9 @@
"command_failure": {
"message": "Command {command} failed. Status: {status}. Message: {message}"
},
"lock_failure": {
"message": "Failed to {command} vehicle. Status: {status}. Message: {message}"
Comment thread
thomasddn marked this conversation as resolved.
},
"no_vehicle": {
"message": "Unable to retrieve vehicle details."
},
Expand Down
197 changes: 197 additions & 0 deletions tests/components/volvo/snapshots/test_lock.ambr
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
# serializer version: 1
# name: test_lock[ex30_2024][lock.volvo_ex30_lock-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'lock',
'entity_category': None,
'entity_id': 'lock.volvo_ex30_lock',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': '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_lock-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Volvo EX30 Lock',
'supported_features': <LockEntityFeature: 0>,
}),
'context': <ANY>,
'entity_id': 'lock.volvo_ex30_lock',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'locked',
})
# ---
# name: test_lock[s90_diesel_2018][lock.volvo_s90_lock-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'lock',
'entity_category': None,
'entity_id': 'lock.volvo_s90_lock',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': '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_lock-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Volvo S90 Lock',
'supported_features': <LockEntityFeature: 0>,
}),
'context': <ANY>,
'entity_id': 'lock.volvo_s90_lock',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'locked',
})
# ---
# name: test_lock[xc40_electric_2024][lock.volvo_xc40_lock-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'lock',
'entity_category': None,
'entity_id': 'lock.volvo_xc40_lock',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': '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_lock-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Volvo XC40 Lock',
'supported_features': <LockEntityFeature: 0>,
}),
'context': <ANY>,
'entity_id': 'lock.volvo_xc40_lock',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'locked',
})
# ---
# name: test_lock[xc90_petrol_2019][lock.volvo_xc90_lock-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'lock',
'entity_category': None,
'entity_id': 'lock.volvo_xc90_lock',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': '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_lock-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Volvo XC90 Lock',
'supported_features': <LockEntityFeature: 0>,
}),
'context': <ANY>,
'entity_id': 'lock.volvo_xc90_lock',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'locked',
})
# ---
Loading
Loading