mirror of
https://github.com/home-assistant/core.git
synced 2025-11-08 10:29:27 +00:00
Add device_tracker platform to Volvo integration (#153437)
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from volvocarsapi.auth import AUTHORIZE_URL, TOKEN_URL
|
from volvocarsapi.auth import AUTHORIZE_URL, TOKEN_URL
|
||||||
from volvocarsapi.scopes import DEFAULT_SCOPES
|
from volvocarsapi.scopes import ALL_SCOPES
|
||||||
|
|
||||||
from homeassistant.components.application_credentials import ClientCredential
|
from homeassistant.components.application_credentials import ClientCredential
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
@@ -33,5 +33,5 @@ class VolvoOAuth2Implementation(LocalOAuth2ImplementationWithPkce):
|
|||||||
def extra_authorize_data(self) -> dict:
|
def extra_authorize_data(self) -> dict:
|
||||||
"""Extra data that needs to be appended to the authorize url."""
|
"""Extra data that needs to be appended to the authorize url."""
|
||||||
return super().extra_authorize_data | {
|
return super().extra_authorize_data | {
|
||||||
"scope": " ".join(DEFAULT_SCOPES),
|
"scope": " ".join(ALL_SCOPES),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from .const import API_NONE_VALUE
|
from .const import API_NONE_VALUE
|
||||||
from .coordinator import VolvoBaseCoordinator, VolvoConfigEntry
|
from .coordinator import VolvoConfigEntry
|
||||||
from .entity import VolvoEntity, VolvoEntityDescription
|
from .entity import VolvoEntity, VolvoEntityDescription
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
@@ -380,16 +380,6 @@ class VolvoBinarySensor(VolvoEntity, BinarySensorEntity):
|
|||||||
|
|
||||||
entity_description: VolvoBinarySensorDescription
|
entity_description: VolvoBinarySensorDescription
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coordinator: VolvoBaseCoordinator,
|
|
||||||
description: VolvoBinarySensorDescription,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize entity."""
|
|
||||||
self._attr_extra_state_attributes = {}
|
|
||||||
|
|
||||||
super().__init__(coordinator, description)
|
|
||||||
|
|
||||||
def _update_state(self, api_field: VolvoCarsApiBaseModel | None) -> None:
|
def _update_state(self, api_field: VolvoCarsApiBaseModel | None) -> None:
|
||||||
"""Update the state of the entity."""
|
"""Update the state of the entity."""
|
||||||
if api_field is None:
|
if api_field is None:
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from typing import Any
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
from volvocarsapi.api import VolvoCarsApi
|
from volvocarsapi.api import VolvoCarsApi
|
||||||
from volvocarsapi.models import VolvoApiException, VolvoCarsVehicle
|
from volvocarsapi.models import VolvoApiException, VolvoCarsVehicle
|
||||||
from volvocarsapi.scopes import DEFAULT_SCOPES
|
from volvocarsapi.scopes import ALL_SCOPES
|
||||||
|
|
||||||
from homeassistant.config_entries import (
|
from homeassistant.config_entries import (
|
||||||
SOURCE_REAUTH,
|
SOURCE_REAUTH,
|
||||||
@@ -59,7 +59,7 @@ class VolvoOAuth2FlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
|||||||
def extra_authorize_data(self) -> dict:
|
def extra_authorize_data(self) -> dict:
|
||||||
"""Extra data that needs to be appended to the authorize url."""
|
"""Extra data that needs to be appended to the authorize url."""
|
||||||
return super().extra_authorize_data | {
|
return super().extra_authorize_data | {
|
||||||
"scope": " ".join(DEFAULT_SCOPES),
|
"scope": " ".join(ALL_SCOPES),
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -3,7 +3,11 @@
|
|||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
|
|
||||||
DOMAIN = "volvo"
|
DOMAIN = "volvo"
|
||||||
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
PLATFORMS: list[Platform] = [
|
||||||
|
Platform.BINARY_SENSOR,
|
||||||
|
Platform.DEVICE_TRACKER,
|
||||||
|
Platform.SENSOR,
|
||||||
|
]
|
||||||
|
|
||||||
API_NONE_VALUE = "UNSPECIFIED"
|
API_NONE_VALUE = "UNSPECIFIED"
|
||||||
CONF_VIN = "vin"
|
CONF_VIN = "vin"
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from volvocarsapi.models import (
|
|||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import DATA_BATTERY_CAPACITY, DOMAIN
|
from .const import DATA_BATTERY_CAPACITY, DOMAIN
|
||||||
@@ -119,7 +119,16 @@ class VolvoBaseIntervalCoordinator(VolvoBaseCoordinator[CoordinatorData]):
|
|||||||
self._api_calls: list[Callable[[], Coroutine[Any, Any, Any]]] = []
|
self._api_calls: list[Callable[[], Coroutine[Any, Any, Any]]] = []
|
||||||
|
|
||||||
async def _async_setup(self) -> None:
|
async def _async_setup(self) -> None:
|
||||||
|
try:
|
||||||
self._api_calls = await self._async_determine_api_calls()
|
self._api_calls = await self._async_determine_api_calls()
|
||||||
|
except VolvoAuthException as err:
|
||||||
|
raise ConfigEntryAuthFailed(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="unauthorized",
|
||||||
|
translation_placeholders={"message": err.message},
|
||||||
|
) from err
|
||||||
|
except VolvoApiException as err:
|
||||||
|
raise ConfigEntryNotReady from err
|
||||||
|
|
||||||
if not self._api_calls:
|
if not self._api_calls:
|
||||||
self.update_interval = None
|
self.update_interval = None
|
||||||
@@ -153,7 +162,9 @@ class VolvoBaseIntervalCoordinator(VolvoBaseCoordinator[CoordinatorData]):
|
|||||||
result.message,
|
result.message,
|
||||||
)
|
)
|
||||||
raise ConfigEntryAuthFailed(
|
raise ConfigEntryAuthFailed(
|
||||||
f"Authentication failed. {result.message}"
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="unauthorized",
|
||||||
|
translation_placeholders={"message": result.message},
|
||||||
) from result
|
) from result
|
||||||
|
|
||||||
if isinstance(result, VolvoApiException):
|
if isinstance(result, VolvoApiException):
|
||||||
@@ -270,14 +281,17 @@ class VolvoSlowIntervalCoordinator(VolvoBaseIntervalCoordinator):
|
|||||||
self,
|
self,
|
||||||
) -> list[Callable[[], Coroutine[Any, Any, Any]]]:
|
) -> list[Callable[[], Coroutine[Any, Any, Any]]]:
|
||||||
api = self.context.api
|
api = self.context.api
|
||||||
|
api_calls: list[Any] = [api.async_get_command_accessibility]
|
||||||
|
|
||||||
|
location = await api.async_get_location()
|
||||||
|
|
||||||
|
if location.get("location") is not None:
|
||||||
|
api_calls.append(api.async_get_location)
|
||||||
|
|
||||||
if self.context.vehicle.has_combustion_engine():
|
if self.context.vehicle.has_combustion_engine():
|
||||||
return [
|
api_calls.append(api.async_get_fuel_status)
|
||||||
api.async_get_command_accessibility,
|
|
||||||
api.async_get_fuel_status,
|
|
||||||
]
|
|
||||||
|
|
||||||
return [api.async_get_command_accessibility]
|
return api_calls
|
||||||
|
|
||||||
|
|
||||||
class VolvoMediumIntervalCoordinator(VolvoBaseIntervalCoordinator):
|
class VolvoMediumIntervalCoordinator(VolvoBaseIntervalCoordinator):
|
||||||
|
|||||||
59
homeassistant/components/volvo/device_tracker.py
Normal file
59
homeassistant/components/volvo/device_tracker.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
"""Volvo device tracker."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from volvocarsapi.models import VolvoCarsApiBaseModel, VolvoCarsLocation
|
||||||
|
|
||||||
|
from homeassistant.components.device_tracker.config_entry import (
|
||||||
|
TrackerEntity,
|
||||||
|
TrackerEntityDescription,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
|
from .coordinator import VolvoConfigEntry
|
||||||
|
from .entity import VolvoEntity, VolvoEntityDescription
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class VolvoTrackerDescription(VolvoEntityDescription, TrackerEntityDescription):
|
||||||
|
"""Describes a Volvo Cars tracker entity."""
|
||||||
|
|
||||||
|
|
||||||
|
_DESCRIPTIONS: tuple[VolvoTrackerDescription, ...] = (
|
||||||
|
VolvoTrackerDescription(
|
||||||
|
key="location",
|
||||||
|
api_field="location",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
_: HomeAssistant,
|
||||||
|
entry: VolvoConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up tracker."""
|
||||||
|
|
||||||
|
coordinators = entry.runtime_data.interval_coordinators
|
||||||
|
async_add_entities(
|
||||||
|
VolvoDeviceTracker(coordinator, description)
|
||||||
|
for coordinator in coordinators
|
||||||
|
for description in _DESCRIPTIONS
|
||||||
|
if description.api_field in coordinator.data
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class VolvoDeviceTracker(VolvoEntity, TrackerEntity):
|
||||||
|
"""Volvo tracker."""
|
||||||
|
|
||||||
|
entity_description: VolvoTrackerDescription
|
||||||
|
|
||||||
|
def _update_state(self, api_field: VolvoCarsApiBaseModel | None) -> None:
|
||||||
|
assert isinstance(api_field, VolvoCarsLocation)
|
||||||
|
|
||||||
|
if api_field.geometry.coordinates and len(api_field.geometry.coordinates) > 1:
|
||||||
|
self._attr_longitude = api_field.geometry.coordinates[0]
|
||||||
|
self._attr_latitude = api_field.geometry.coordinates[1]
|
||||||
@@ -307,6 +307,9 @@
|
|||||||
"dc": "mdi:current-dc"
|
"dc": "mdi:current-dc"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"direction": {
|
||||||
|
"default": "mdi:compass-outline"
|
||||||
|
},
|
||||||
"distance_to_empty_battery": {
|
"distance_to_empty_battery": {
|
||||||
"default": "mdi:battery-outline"
|
"default": "mdi:battery-outline"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ from __future__ import annotations
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, cast
|
from typing import cast
|
||||||
|
|
||||||
from volvocarsapi.models import (
|
from volvocarsapi.models import (
|
||||||
VolvoCarsApiBaseModel,
|
VolvoCarsApiBaseModel,
|
||||||
|
VolvoCarsLocation,
|
||||||
VolvoCarsValue,
|
VolvoCarsValue,
|
||||||
VolvoCarsValueField,
|
VolvoCarsValueField,
|
||||||
VolvoCarsValueStatusField,
|
VolvoCarsValueStatusField,
|
||||||
@@ -21,6 +22,7 @@ from homeassistant.components.sensor import (
|
|||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
DEGREE,
|
||||||
PERCENTAGE,
|
PERCENTAGE,
|
||||||
EntityCategory,
|
EntityCategory,
|
||||||
UnitOfElectricCurrent,
|
UnitOfElectricCurrent,
|
||||||
@@ -34,6 +36,7 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
from homeassistant.helpers.typing import StateType
|
||||||
|
|
||||||
from .const import API_NONE_VALUE, DATA_BATTERY_CAPACITY
|
from .const import API_NONE_VALUE, DATA_BATTERY_CAPACITY
|
||||||
from .coordinator import VolvoConfigEntry
|
from .coordinator import VolvoConfigEntry
|
||||||
@@ -47,25 +50,31 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
class VolvoSensorDescription(VolvoEntityDescription, SensorEntityDescription):
|
class VolvoSensorDescription(VolvoEntityDescription, SensorEntityDescription):
|
||||||
"""Describes a Volvo sensor entity."""
|
"""Describes a Volvo sensor entity."""
|
||||||
|
|
||||||
value_fn: Callable[[VolvoCarsValue], Any] | None = None
|
value_fn: Callable[[VolvoCarsApiBaseModel], StateType] | None = None
|
||||||
|
|
||||||
|
|
||||||
def _availability_status(field: VolvoCarsValue) -> str:
|
def _availability_status(field: VolvoCarsApiBaseModel) -> str:
|
||||||
reason = field.get("unavailable_reason")
|
reason = field.get("unavailable_reason")
|
||||||
return reason if reason else str(field.value)
|
|
||||||
|
if reason:
|
||||||
|
return str(reason)
|
||||||
|
|
||||||
|
if isinstance(field, VolvoCarsValue):
|
||||||
|
return str(field.value)
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def _calculate_time_to_service(field: VolvoCarsValue) -> int:
|
def _calculate_time_to_service(field: VolvoCarsApiBaseModel) -> int:
|
||||||
|
if not isinstance(field, VolvoCarsValueField):
|
||||||
|
return 0
|
||||||
|
|
||||||
value = int(field.value)
|
value = int(field.value)
|
||||||
|
|
||||||
# Always express value in days
|
# Always express value in days
|
||||||
if isinstance(field, VolvoCarsValueField) and field.unit == "months":
|
return value * 30 if field.unit == "months" else value
|
||||||
return value * 30
|
|
||||||
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def _charging_power_value(field: VolvoCarsValue) -> int:
|
def _charging_power_value(field: VolvoCarsApiBaseModel) -> int:
|
||||||
return (
|
return (
|
||||||
field.value
|
field.value
|
||||||
if isinstance(field, VolvoCarsValueStatusField) and isinstance(field.value, int)
|
if isinstance(field, VolvoCarsValueStatusField) and isinstance(field.value, int)
|
||||||
@@ -73,8 +82,8 @@ def _charging_power_value(field: VolvoCarsValue) -> int:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _charging_power_status_value(field: VolvoCarsValue) -> str | None:
|
def _charging_power_status_value(field: VolvoCarsApiBaseModel) -> str | None:
|
||||||
status = cast(str, field.value)
|
status = cast(str, field.value) if isinstance(field, VolvoCarsValue) else ""
|
||||||
|
|
||||||
if status.lower() in _CHARGING_POWER_STATUS_OPTIONS:
|
if status.lower() in _CHARGING_POWER_STATUS_OPTIONS:
|
||||||
return status
|
return status
|
||||||
@@ -86,6 +95,10 @@ def _charging_power_status_value(field: VolvoCarsValue) -> str | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _direction_value(field: VolvoCarsApiBaseModel) -> str | None:
|
||||||
|
return field.properties.heading if isinstance(field, VolvoCarsLocation) else None
|
||||||
|
|
||||||
|
|
||||||
_CHARGING_POWER_STATUS_OPTIONS = [
|
_CHARGING_POWER_STATUS_OPTIONS = [
|
||||||
"fault",
|
"fault",
|
||||||
"power_available_but_not_activated",
|
"power_available_but_not_activated",
|
||||||
@@ -245,6 +258,14 @@ _DESCRIPTIONS: tuple[VolvoSensorDescription, ...] = (
|
|||||||
"none",
|
"none",
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
# location endpoint
|
||||||
|
VolvoSensorDescription(
|
||||||
|
key="direction",
|
||||||
|
api_field="location",
|
||||||
|
native_unit_of_measurement=DEGREE,
|
||||||
|
suggested_display_precision=0,
|
||||||
|
value_fn=_direction_value,
|
||||||
|
),
|
||||||
# statistics endpoint
|
# statistics endpoint
|
||||||
# We're not using `electricRange` from the energy state endpoint because
|
# We're not using `electricRange` from the energy state endpoint because
|
||||||
# the official app seems to use `distanceToEmptyBattery`.
|
# the official app seems to use `distanceToEmptyBattery`.
|
||||||
@@ -380,13 +401,12 @@ class VolvoSensor(VolvoEntity, SensorEntity):
|
|||||||
self._attr_native_value = None
|
self._attr_native_value = None
|
||||||
return
|
return
|
||||||
|
|
||||||
assert isinstance(api_field, VolvoCarsValue)
|
native_value = None
|
||||||
|
|
||||||
native_value = (
|
if self.entity_description.value_fn:
|
||||||
api_field.value
|
native_value = self.entity_description.value_fn(api_field)
|
||||||
if self.entity_description.value_fn is None
|
elif isinstance(api_field, VolvoCarsValue):
|
||||||
else self.entity_description.value_fn(api_field)
|
native_value = api_field.value
|
||||||
)
|
|
||||||
|
|
||||||
if self.device_class == SensorDeviceClass.ENUM and native_value:
|
if self.device_class == SensorDeviceClass.ENUM and native_value:
|
||||||
# Entities having an "unknown" value should report None as the state
|
# Entities having an "unknown" value should report None as the state
|
||||||
|
|||||||
@@ -268,6 +268,9 @@
|
|||||||
"none": "None"
|
"none": "None"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"direction": {
|
||||||
|
"name": "Direction"
|
||||||
|
},
|
||||||
"distance_to_empty_battery": {
|
"distance_to_empty_battery": {
|
||||||
"name": "Distance to empty battery"
|
"name": "Distance to empty battery"
|
||||||
},
|
},
|
||||||
|
|||||||
209
tests/components/volvo/snapshots/test_device_tracker.ambr
Normal file
209
tests/components/volvo/snapshots/test_device_tracker.ambr
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
# serializer version: 1
|
||||||
|
# name: test_device_tracker[ex30_2024][device_tracker.volvo_ex30_none-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': 'device_tracker',
|
||||||
|
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||||
|
'entity_id': 'device_tracker.volvo_ex30_none',
|
||||||
|
'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': None,
|
||||||
|
'platform': 'volvo',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'location',
|
||||||
|
'unique_id': 'yv1abcdefg1234567_location',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_device_tracker[ex30_2024][device_tracker.volvo_ex30_none-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'Volvo EX30 None',
|
||||||
|
'gps_accuracy': 0,
|
||||||
|
'latitude': 57.72537482589284,
|
||||||
|
'longitude': 11.849843629550225,
|
||||||
|
'source_type': <SourceType.GPS: 'gps'>,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'device_tracker.volvo_ex30_none',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'not_home',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_device_tracker[s90_diesel_2018][device_tracker.volvo_s90_none-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': 'device_tracker',
|
||||||
|
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||||
|
'entity_id': 'device_tracker.volvo_s90_none',
|
||||||
|
'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': None,
|
||||||
|
'platform': 'volvo',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'location',
|
||||||
|
'unique_id': 'yv1abcdefg1234567_location',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_device_tracker[s90_diesel_2018][device_tracker.volvo_s90_none-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'Volvo S90 None',
|
||||||
|
'gps_accuracy': 0,
|
||||||
|
'latitude': 57.72537482589284,
|
||||||
|
'longitude': 11.849843629550225,
|
||||||
|
'source_type': <SourceType.GPS: 'gps'>,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'device_tracker.volvo_s90_none',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'not_home',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_device_tracker[xc40_electric_2024][device_tracker.volvo_xc40_none-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': 'device_tracker',
|
||||||
|
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||||
|
'entity_id': 'device_tracker.volvo_xc40_none',
|
||||||
|
'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': None,
|
||||||
|
'platform': 'volvo',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'location',
|
||||||
|
'unique_id': 'yv1abcdefg1234567_location',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_device_tracker[xc40_electric_2024][device_tracker.volvo_xc40_none-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'Volvo XC40 None',
|
||||||
|
'gps_accuracy': 0,
|
||||||
|
'latitude': 57.72537482589284,
|
||||||
|
'longitude': 11.849843629550225,
|
||||||
|
'source_type': <SourceType.GPS: 'gps'>,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'device_tracker.volvo_xc40_none',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'not_home',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_device_tracker[xc90_petrol_2019][device_tracker.volvo_xc90_none-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': 'device_tracker',
|
||||||
|
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||||
|
'entity_id': 'device_tracker.volvo_xc90_none',
|
||||||
|
'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': None,
|
||||||
|
'platform': 'volvo',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'location',
|
||||||
|
'unique_id': 'yv1abcdefg1234567_location',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_device_tracker[xc90_petrol_2019][device_tracker.volvo_xc90_none-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'Volvo XC90 None',
|
||||||
|
'gps_accuracy': 0,
|
||||||
|
'latitude': 57.72537482589284,
|
||||||
|
'longitude': 11.849843629550225,
|
||||||
|
'source_type': <SourceType.GPS: 'gps'>,
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'device_tracker.volvo_xc90_none',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'not_home',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
@@ -194,6 +194,23 @@
|
|||||||
'unit': None,
|
'unit': None,
|
||||||
'value': 'AVAILABLE',
|
'value': 'AVAILABLE',
|
||||||
}),
|
}),
|
||||||
|
'location': dict({
|
||||||
|
'extra_data': dict({
|
||||||
|
}),
|
||||||
|
'geometry': dict({
|
||||||
|
'coordinates': '**REDACTED**',
|
||||||
|
'extra_data': dict({
|
||||||
|
'type': 'Point',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'properties': dict({
|
||||||
|
'extra_data': dict({
|
||||||
|
}),
|
||||||
|
'heading': '**REDACTED**',
|
||||||
|
'timestamp': '2024-12-30T15:00:00+00:00',
|
||||||
|
}),
|
||||||
|
'type': 'Feature',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
'Volvo very slow interval coordinator': dict({
|
'Volvo very slow interval coordinator': dict({
|
||||||
'averageEnergyConsumption': dict({
|
'averageEnergyConsumption': dict({
|
||||||
|
|||||||
@@ -476,6 +476,58 @@
|
|||||||
'state': 'ac',
|
'state': 'ac',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
|
# name: test_sensor[ex30_2024][sensor.volvo_ex30_direction-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': 'sensor',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'sensor.volvo_ex30_direction',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
'sensor': dict({
|
||||||
|
'suggested_display_precision': 0,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'original_device_class': None,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'Direction',
|
||||||
|
'platform': 'volvo',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'direction',
|
||||||
|
'unique_id': 'yv1abcdefg1234567_direction',
|
||||||
|
'unit_of_measurement': '°',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_sensor[ex30_2024][sensor.volvo_ex30_direction-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'Volvo EX30 Direction',
|
||||||
|
'unit_of_measurement': '°',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.volvo_ex30_direction',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '90',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
# name: test_sensor[ex30_2024][sensor.volvo_ex30_distance_to_empty_battery-entry]
|
# name: test_sensor[ex30_2024][sensor.volvo_ex30_distance_to_empty_battery-entry]
|
||||||
EntityRegistryEntrySnapshot({
|
EntityRegistryEntrySnapshot({
|
||||||
'aliases': set({
|
'aliases': set({
|
||||||
@@ -1263,6 +1315,58 @@
|
|||||||
'state': 'available',
|
'state': 'available',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
|
# name: test_sensor[s90_diesel_2018][sensor.volvo_s90_direction-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': 'sensor',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'sensor.volvo_s90_direction',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
'sensor': dict({
|
||||||
|
'suggested_display_precision': 0,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'original_device_class': None,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'Direction',
|
||||||
|
'platform': 'volvo',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'direction',
|
||||||
|
'unique_id': 'yv1abcdefg1234567_direction',
|
||||||
|
'unit_of_measurement': '°',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_sensor[s90_diesel_2018][sensor.volvo_s90_direction-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'Volvo S90 Direction',
|
||||||
|
'unit_of_measurement': '°',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.volvo_s90_direction',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '90',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
# name: test_sensor[s90_diesel_2018][sensor.volvo_s90_distance_to_empty_tank-entry]
|
# name: test_sensor[s90_diesel_2018][sensor.volvo_s90_distance_to_empty_tank-entry]
|
||||||
EntityRegistryEntrySnapshot({
|
EntityRegistryEntrySnapshot({
|
||||||
'aliases': set({
|
'aliases': set({
|
||||||
@@ -2411,6 +2515,58 @@
|
|||||||
'state': 'ac',
|
'state': 'ac',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
|
# name: test_sensor[xc40_electric_2024][sensor.volvo_xc40_direction-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': 'sensor',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'sensor.volvo_xc40_direction',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
'sensor': dict({
|
||||||
|
'suggested_display_precision': 0,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'original_device_class': None,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'Direction',
|
||||||
|
'platform': 'volvo',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'direction',
|
||||||
|
'unique_id': 'yv1abcdefg1234567_direction',
|
||||||
|
'unit_of_measurement': '°',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_sensor[xc40_electric_2024][sensor.volvo_xc40_direction-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'Volvo XC40 Direction',
|
||||||
|
'unit_of_measurement': '°',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.volvo_xc40_direction',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '90',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
# name: test_sensor[xc40_electric_2024][sensor.volvo_xc40_distance_to_empty_battery-entry]
|
# name: test_sensor[xc40_electric_2024][sensor.volvo_xc40_distance_to_empty_battery-entry]
|
||||||
EntityRegistryEntrySnapshot({
|
EntityRegistryEntrySnapshot({
|
||||||
'aliases': set({
|
'aliases': set({
|
||||||
@@ -3377,6 +3533,58 @@
|
|||||||
'state': 'idle',
|
'state': 'idle',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
|
# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_direction-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': 'sensor',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'sensor.volvo_xc60_direction',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
'sensor': dict({
|
||||||
|
'suggested_display_precision': 0,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'original_device_class': None,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'Direction',
|
||||||
|
'platform': 'volvo',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'direction',
|
||||||
|
'unique_id': 'yv1abcdefg1234567_direction',
|
||||||
|
'unit_of_measurement': '°',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_direction-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'Volvo XC60 Direction',
|
||||||
|
'unit_of_measurement': '°',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.volvo_xc60_direction',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '90',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_distance_to_empty_battery-entry]
|
# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_distance_to_empty_battery-entry]
|
||||||
EntityRegistryEntrySnapshot({
|
EntityRegistryEntrySnapshot({
|
||||||
'aliases': set({
|
'aliases': set({
|
||||||
@@ -4164,6 +4372,58 @@
|
|||||||
'state': 'available',
|
'state': 'available',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
|
# name: test_sensor[xc90_petrol_2019][sensor.volvo_xc90_direction-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': 'sensor',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'sensor.volvo_xc90_direction',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
'sensor': dict({
|
||||||
|
'suggested_display_precision': 0,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'original_device_class': None,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'Direction',
|
||||||
|
'platform': 'volvo',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'direction',
|
||||||
|
'unique_id': 'yv1abcdefg1234567_direction',
|
||||||
|
'unit_of_measurement': '°',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_sensor[xc90_petrol_2019][sensor.volvo_xc90_direction-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'Volvo XC90 Direction',
|
||||||
|
'unit_of_measurement': '°',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.volvo_xc90_direction',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '90',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
# name: test_sensor[xc90_petrol_2019][sensor.volvo_xc90_distance_to_empty_tank-entry]
|
# name: test_sensor[xc90_petrol_2019][sensor.volvo_xc90_distance_to_empty_tank-entry]
|
||||||
EntityRegistryEntrySnapshot({
|
EntityRegistryEntrySnapshot({
|
||||||
'aliases': set({
|
'aliases': set({
|
||||||
@@ -5200,6 +5460,58 @@
|
|||||||
'state': 'none',
|
'state': 'none',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
|
# name: test_sensor[xc90_phev_2024][sensor.volvo_xc90_direction-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': 'sensor',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'sensor.volvo_xc90_direction',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
'sensor': dict({
|
||||||
|
'suggested_display_precision': 0,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
'original_device_class': None,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'Direction',
|
||||||
|
'platform': 'volvo',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': 'direction',
|
||||||
|
'unique_id': 'yv1abcdefg1234567_direction',
|
||||||
|
'unit_of_measurement': '°',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_sensor[xc90_phev_2024][sensor.volvo_xc90_direction-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'Volvo XC90 Direction',
|
||||||
|
'unit_of_measurement': '°',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.volvo_xc90_direction',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '90',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
# name: test_sensor[xc90_phev_2024][sensor.volvo_xc90_distance_to_empty_battery-entry]
|
# name: test_sensor[xc90_phev_2024][sensor.volvo_xc90_distance_to_empty_battery-entry]
|
||||||
EntityRegistryEntrySnapshot({
|
EntityRegistryEntrySnapshot({
|
||||||
'aliases': set({
|
'aliases': set({
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import pytest
|
|||||||
from volvocarsapi.api import VolvoCarsApi
|
from volvocarsapi.api import VolvoCarsApi
|
||||||
from volvocarsapi.auth import AUTHORIZE_URL, TOKEN_URL
|
from volvocarsapi.auth import AUTHORIZE_URL, TOKEN_URL
|
||||||
from volvocarsapi.models import VolvoApiException, VolvoCarsVehicle
|
from volvocarsapi.models import VolvoApiException, VolvoCarsVehicle
|
||||||
from volvocarsapi.scopes import DEFAULT_SCOPES
|
from volvocarsapi.scopes import ALL_SCOPES
|
||||||
from yarl import URL
|
from yarl import URL
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
@@ -251,7 +251,7 @@ async def config_flow(
|
|||||||
assert result_url.query["state"] == state
|
assert result_url.query["state"] == state
|
||||||
assert result_url.query["code_challenge"]
|
assert result_url.query["code_challenge"]
|
||||||
assert result_url.query["code_challenge_method"] == "S256"
|
assert result_url.query["code_challenge_method"] == "S256"
|
||||||
assert result_url.query["scope"] == " ".join(DEFAULT_SCOPES)
|
assert result_url.query["scope"] == " ".join(ALL_SCOPES)
|
||||||
|
|
||||||
client = await hass_client_no_auth()
|
client = await hass_client_no_auth()
|
||||||
resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
|
resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from volvocarsapi.models import (
|
|||||||
VolvoCarsValueField,
|
VolvoCarsValueField,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from homeassistant.components.volvo.const import DOMAIN
|
||||||
from homeassistant.components.volvo.coordinator import VERY_SLOW_INTERVAL
|
from homeassistant.components.volvo.coordinator import VERY_SLOW_INTERVAL
|
||||||
from homeassistant.const import STATE_UNAVAILABLE
|
from homeassistant.const import STATE_UNAVAILABLE
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
@@ -123,7 +124,7 @@ async def test_update_coordinator_all_error(
|
|||||||
freezer.tick(timedelta(minutes=VERY_SLOW_INTERVAL))
|
freezer.tick(timedelta(minutes=VERY_SLOW_INTERVAL))
|
||||||
async_fire_time_changed(hass)
|
async_fire_time_changed(hass)
|
||||||
await hass.async_block_till_done(wait_background_tasks=True)
|
await hass.async_block_till_done(wait_background_tasks=True)
|
||||||
for state in hass.states.async_all():
|
for state in hass.states.async_all(domain_filter=DOMAIN):
|
||||||
assert state.state == STATE_UNAVAILABLE
|
assert state.state == STATE_UNAVAILABLE
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
33
tests/components/volvo/test_device_tracker.py
Normal file
33
tests/components/volvo/test_device_tracker.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
"""Test Volvo device tracker."""
|
||||||
|
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from syrupy.assertion import SnapshotAssertion
|
||||||
|
|
||||||
|
from homeassistant.const import Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
|
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_device_tracker(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
setup_integration: Callable[[], Awaitable[bool]],
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test device tracker."""
|
||||||
|
|
||||||
|
with patch("homeassistant.components.volvo.PLATFORMS", [Platform.DEVICE_TRACKER]):
|
||||||
|
assert await setup_integration()
|
||||||
|
|
||||||
|
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||||
@@ -1,13 +1,19 @@
|
|||||||
"""Test Volvo sensors."""
|
"""Test Volvo sensors."""
|
||||||
|
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
from unittest.mock import patch
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from syrupy.assertion import SnapshotAssertion
|
from syrupy.assertion import SnapshotAssertion
|
||||||
|
from volvocarsapi.api import VolvoCarsApi
|
||||||
|
from volvocarsapi.models import (
|
||||||
|
VolvoCarsErrorResult,
|
||||||
|
VolvoCarsValue,
|
||||||
|
VolvoCarsValueField,
|
||||||
|
)
|
||||||
|
|
||||||
from homeassistant.components.volvo.const import DOMAIN
|
from homeassistant.components.volvo.const import DOMAIN
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import STATE_UNKNOWN, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
@@ -116,3 +122,96 @@ async def test_unique_ids(
|
|||||||
assert await setup_integration()
|
assert await setup_integration()
|
||||||
|
|
||||||
assert f"Platform {DOMAIN} does not generate unique IDs" not in caplog.text
|
assert f"Platform {DOMAIN} does not generate unique IDs" not in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_availability_status_reason(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
setup_integration: Callable[[], Awaitable[bool]],
|
||||||
|
mock_api: VolvoCarsApi,
|
||||||
|
) -> None:
|
||||||
|
"""Test availability_status entity returns unavailable reason."""
|
||||||
|
|
||||||
|
mock_method: AsyncMock = mock_api.async_get_command_accessibility
|
||||||
|
mock_method.return_value["availabilityStatus"] = VolvoCarsValue(
|
||||||
|
value="UNAVAILABLE", extra_data={"unavailable_reason": "no_internet"}
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("homeassistant.components.volvo.PLATFORMS", [Platform.SENSOR]):
|
||||||
|
assert await setup_integration()
|
||||||
|
|
||||||
|
state = hass.states.get("sensor.volvo_xc40_car_connection")
|
||||||
|
assert state.state == "no_internet"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_time_to_service_non_value_field(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
setup_integration: Callable[[], Awaitable[bool]],
|
||||||
|
mock_api: VolvoCarsApi,
|
||||||
|
) -> None:
|
||||||
|
"""Test time_to_service entity with non-VolvoCarsValueField returns 0."""
|
||||||
|
|
||||||
|
mock_method: AsyncMock = mock_api.async_get_diagnostics
|
||||||
|
mock_method.return_value["timeToService"] = VolvoCarsErrorResult(message="invalid")
|
||||||
|
|
||||||
|
with patch("homeassistant.components.volvo.PLATFORMS", [Platform.SENSOR]):
|
||||||
|
assert await setup_integration()
|
||||||
|
|
||||||
|
state = hass.states.get("sensor.volvo_xc40_time_to_service")
|
||||||
|
assert state.state == "0"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_time_to_service_months_conversion(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
setup_integration: Callable[[], Awaitable[bool]],
|
||||||
|
mock_api: VolvoCarsApi,
|
||||||
|
) -> None:
|
||||||
|
"""Test time_to_service entity converts months to days."""
|
||||||
|
|
||||||
|
mock_method: AsyncMock = mock_api.async_get_diagnostics
|
||||||
|
mock_method.return_value["timeToService"] = VolvoCarsValueField(
|
||||||
|
value=3, unit="months"
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("homeassistant.components.volvo.PLATFORMS", [Platform.SENSOR]):
|
||||||
|
assert await setup_integration()
|
||||||
|
|
||||||
|
state = hass.states.get("sensor.volvo_xc40_time_to_service")
|
||||||
|
assert state.state == "90"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_charging_power_value_fallback(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
setup_integration: Callable[[], Awaitable[bool]],
|
||||||
|
mock_api: VolvoCarsApi,
|
||||||
|
) -> None:
|
||||||
|
"""Test charging_power entity returns 0 for invalid field types."""
|
||||||
|
|
||||||
|
mock_method: AsyncMock = mock_api.async_get_energy_state
|
||||||
|
mock_method.return_value["chargingPower"] = VolvoCarsErrorResult(message="invalid")
|
||||||
|
|
||||||
|
with patch("homeassistant.components.volvo.PLATFORMS", [Platform.SENSOR]):
|
||||||
|
assert await setup_integration()
|
||||||
|
|
||||||
|
state = hass.states.get("sensor.volvo_xc40_charging_power")
|
||||||
|
assert state.state == "0"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_charging_power_status_unknown_value(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
setup_integration: Callable[[], Awaitable[bool]],
|
||||||
|
mock_api: VolvoCarsApi,
|
||||||
|
caplog: pytest.LogCaptureFixture,
|
||||||
|
) -> None:
|
||||||
|
"""Test charging_power_status entity with unknown status logs warning."""
|
||||||
|
|
||||||
|
mock_method: AsyncMock = mock_api.async_get_energy_state
|
||||||
|
mock_method.return_value["chargerPowerStatus"] = VolvoCarsValue(
|
||||||
|
value="unknown_status"
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch("homeassistant.components.volvo.PLATFORMS", [Platform.SENSOR]):
|
||||||
|
assert await setup_integration()
|
||||||
|
|
||||||
|
state = hass.states.get("sensor.volvo_xc40_charging_power_status")
|
||||||
|
assert state.state == STATE_UNKNOWN
|
||||||
|
assert "Unknown value 'unknown_status' for charging_power_status" in caplog.text
|
||||||
|
|||||||
Reference in New Issue
Block a user