Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
4 changes: 2 additions & 2 deletions homeassistant/components/volvo/application_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from __future__ import annotations

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.core import HomeAssistant
Expand Down Expand Up @@ -33,5 +33,5 @@ class VolvoOAuth2Implementation(LocalOAuth2ImplementationWithPkce):
def extra_authorize_data(self) -> dict:
"""Extra data that needs to be appended to the authorize url."""
return super().extra_authorize_data | {
"scope": " ".join(DEFAULT_SCOPES),
"scope": " ".join(ALL_SCOPES),
}
12 changes: 1 addition & 11 deletions homeassistant/components/volvo/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from .const import API_NONE_VALUE
from .coordinator import VolvoBaseCoordinator, VolvoConfigEntry
from .coordinator import VolvoConfigEntry
from .entity import VolvoEntity, VolvoEntityDescription

PARALLEL_UPDATES = 0
Expand Down Expand Up @@ -380,16 +380,6 @@ class VolvoBinarySensor(VolvoEntity, BinarySensorEntity):

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:
"""Update the state of the entity."""
if api_field is None:
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/volvo/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import voluptuous as vol
from volvocarsapi.api import VolvoCarsApi
from volvocarsapi.models import VolvoApiException, VolvoCarsVehicle
from volvocarsapi.scopes import DEFAULT_SCOPES
from volvocarsapi.scopes import ALL_SCOPES

from homeassistant.config_entries import (
SOURCE_REAUTH,
Expand Down Expand Up @@ -59,7 +59,7 @@ def __init__(self) -> None:
def extra_authorize_data(self) -> dict:
"""Extra data that needs to be appended to the authorize url."""
return super().extra_authorize_data | {
"scope": " ".join(DEFAULT_SCOPES),
"scope": " ".join(ALL_SCOPES),
}

@property
Expand Down
6 changes: 5 additions & 1 deletion homeassistant/components/volvo/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
from homeassistant.const import Platform

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"
CONF_VIN = "vin"
Expand Down
24 changes: 17 additions & 7 deletions homeassistant/components/volvo/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

from homeassistant.config_entries import ConfigEntry
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 .const import DATA_BATTERY_CAPACITY, DOMAIN
Expand Down Expand Up @@ -122,7 +122,14 @@ def __init__(
self._api_calls: list[Callable[[], Coroutine[Any, Any, Any]]] = []

async def _async_setup(self) -> None:
self._api_calls = await self._async_determine_api_calls()
try:
self._api_calls = await self._async_determine_api_calls()
except VolvoAuthException as err:
raise ConfigEntryAuthFailed(
f"Authentication failed. {err.message}"
Comment thread
thomasddn marked this conversation as resolved.
Outdated
) from err
except VolvoApiException as err:
raise ConfigEntryNotReady from err

if not self._api_calls:
self.update_interval = None
Expand Down Expand Up @@ -273,14 +280,17 @@ async def _async_determine_api_calls(
self,
) -> list[Callable[[], Coroutine[Any, Any, Any]]]:
api = self.context.api
api_calls: list[Any] = [api.async_get_command_accessibility]

location = await api.async_get_location()
Comment thread
thomasddn marked this conversation as resolved.

if location.get("location") is not None:
api_calls.append(api.async_get_location)

if self.context.vehicle.has_combustion_engine():
return [
api.async_get_command_accessibility,
api.async_get_fuel_status,
]
api_calls.append(api.async_get_fuel_status)

return [api.async_get_command_accessibility]
return api_calls


class VolvoMediumIntervalCoordinator(VolvoBaseIntervalCoordinator):
Expand Down
59 changes: 59 additions & 0 deletions homeassistant/components/volvo/device_tracker.py
Original file line number Diff line number Diff line change
@@ -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)
Comment thread
thomasddn marked this conversation as resolved.
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]
3 changes: 3 additions & 0 deletions homeassistant/components/volvo/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,9 @@
"dc": "mdi:current-dc"
}
},
"direction": {
"default": "mdi:compass-outline"
},
"distance_to_empty_battery": {
"default": "mdi:battery-outline"
},
Expand Down
55 changes: 37 additions & 18 deletions homeassistant/components/volvo/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from volvocarsapi.models import (
VolvoCarsApiBaseModel,
VolvoCarsLocation,
VolvoCarsValue,
VolvoCarsValueField,
VolvoCarsValueStatusField,
Expand All @@ -21,6 +22,7 @@
SensorStateClass,
)
from homeassistant.const import (
DEGREE,
PERCENTAGE,
EntityCategory,
UnitOfElectricCurrent,
Expand All @@ -47,34 +49,40 @@
class VolvoSensorDescription(VolvoEntityDescription, SensorEntityDescription):
"""Describes a Volvo sensor entity."""

value_fn: Callable[[VolvoCarsValue], Any] | None = None
value_fn: Callable[[VolvoCarsApiBaseModel], Any] | None = None
Comment thread
thomasddn marked this conversation as resolved.
Outdated


def _availability_status(field: VolvoCarsValue) -> str:
def _availability_status(field: VolvoCarsApiBaseModel) -> str:
reason = field.get("unavailable_reason")
return reason if reason else str(field.value)

if reason:
return str(reason)

def _calculate_time_to_service(field: VolvoCarsValue) -> int:
value = int(field.value)
if isinstance(field, VolvoCarsValue):
return str(field.value)

return ""

# Always express value in days
if isinstance(field, VolvoCarsValueField) and field.unit == "months":
return value * 30

return value
def _calculate_time_to_service(field: VolvoCarsApiBaseModel) -> int:
if not isinstance(field, VolvoCarsValueField):
return 0

value = int(field.value)
# Always express value in days
return value * 30 if field.unit == "months" else value


def _charging_power_value(field: VolvoCarsValue) -> int:
def _charging_power_value(field: VolvoCarsApiBaseModel) -> int:
return (
field.value
if isinstance(field, VolvoCarsValueStatusField) and isinstance(field.value, int)
else 0
)


def _charging_power_status_value(field: VolvoCarsValue) -> str | None:
status = cast(str, field.value)
def _charging_power_status_value(field: VolvoCarsApiBaseModel) -> str | None:
status = cast(str, field.value) if isinstance(field, VolvoCarsValue) else ""

if status.lower() in _CHARGING_POWER_STATUS_OPTIONS:
return status
Expand All @@ -86,6 +94,10 @@ def _charging_power_status_value(field: VolvoCarsValue) -> str | None:
return None


def _direction_value(field: VolvoCarsApiBaseModel) -> str | None:
return field.properties.heading if isinstance(field, VolvoCarsLocation) else None


_CHARGING_POWER_STATUS_OPTIONS = [
"fault",
"power_available_but_not_activated",
Expand Down Expand Up @@ -245,6 +257,14 @@ def _charging_power_status_value(field: VolvoCarsValue) -> str | 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
# We're not using `electricRange` from the energy state endpoint because
# the official app seems to use `distanceToEmptyBattery`.
Expand Down Expand Up @@ -380,13 +400,12 @@ def _update_state(self, api_field: VolvoCarsApiBaseModel | None) -> None:
self._attr_native_value = None
return

assert isinstance(api_field, VolvoCarsValue)
native_value = None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a bit trouble to understand why you assign this variable, whilst it will be outputted later on directly to self._attr_native_value. Can't the _attr be used?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm using a variable, because in the statements below, the value could still be transformed before it is assigned to self._attr_native_value. It's probably personal taste, but I like that better than throwing self._attr_native_value everywhere around. 😄

I could move the whole block to a method if you'd prefer that.


native_value = (
api_field.value
if self.entity_description.value_fn is None
else self.entity_description.value_fn(api_field)
)
if self.entity_description.value_fn:
native_value = self.entity_description.value_fn(api_field)
elif isinstance(api_field, VolvoCarsValue):
native_value = api_field.value

if self.device_class == SensorDeviceClass.ENUM and native_value:
# Entities having an "unknown" value should report None as the state
Expand Down
3 changes: 3 additions & 0 deletions homeassistant/components/volvo/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,9 @@
"none": "None"
}
},
"direction": {
"name": "Direction"
},
"distance_to_empty_battery": {
"name": "Distance to empty battery"
},
Expand Down
Loading
Loading