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
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
30 changes: 22 additions & 8 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 @@ -119,7 +119,16 @@ 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(
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:
self.update_interval = None
Expand Down Expand Up @@ -153,7 +162,9 @@ async def _async_update_data(self) -> CoordinatorData:
result.message,
)
raise ConfigEntryAuthFailed(
f"Authentication failed. {result.message}"
translation_domain=DOMAIN,
translation_key="unauthorized",
translation_placeholders={"message": result.message},
) from result

if isinstance(result, VolvoApiException):
Expand Down Expand Up @@ -270,14 +281,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
58 changes: 39 additions & 19 deletions homeassistant/components/volvo/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
from collections.abc import Callable
from dataclasses import dataclass
import logging
from typing import Any, cast
from typing import cast

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 @@ -34,6 +36,7 @@
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType

from .const import API_NONE_VALUE, DATA_BATTERY_CAPACITY
from .coordinator import VolvoConfigEntry
Expand All @@ -47,34 +50,40 @@
class VolvoSensorDescription(VolvoEntityDescription, SensorEntityDescription):
"""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")
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 +95,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 +258,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 +401,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