Add binary sensors to Volvo integration (#150127)

This commit is contained in:
Thomas D
2025-08-26 23:52:13 +02:00
committed by GitHub
parent 0139407f52
commit 11f1e376c4
9 changed files with 9423 additions and 31 deletions

View File

@@ -25,6 +25,7 @@ from .api import VolvoAuth
from .const import CONF_VIN, DOMAIN, PLATFORMS from .const import CONF_VIN, DOMAIN, PLATFORMS
from .coordinator import ( from .coordinator import (
VolvoConfigEntry, VolvoConfigEntry,
VolvoFastIntervalCoordinator,
VolvoMediumIntervalCoordinator, VolvoMediumIntervalCoordinator,
VolvoSlowIntervalCoordinator, VolvoSlowIntervalCoordinator,
VolvoVerySlowIntervalCoordinator, VolvoVerySlowIntervalCoordinator,
@@ -38,7 +39,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: VolvoConfigEntry) -> boo
vehicle = await _async_load_vehicle(api) vehicle = await _async_load_vehicle(api)
# Order is important! Faster intervals must come first. # Order is important! Faster intervals must come first.
# Different interval coordinators are in place to keep the number
# of requests under 5000 per day. This lets users use the same
# API key for two vehicles (as the limit is 10000 per day).
coordinators = ( coordinators = (
VolvoFastIntervalCoordinator(hass, entry, api, vehicle),
VolvoMediumIntervalCoordinator(hass, entry, api, vehicle), VolvoMediumIntervalCoordinator(hass, entry, api, vehicle),
VolvoSlowIntervalCoordinator(hass, entry, api, vehicle), VolvoSlowIntervalCoordinator(hass, entry, api, vehicle),
VolvoVerySlowIntervalCoordinator(hass, entry, api, vehicle), VolvoVerySlowIntervalCoordinator(hass, entry, api, vehicle),

View File

@@ -0,0 +1,408 @@
"""Volvo binary sensors."""
from __future__ import annotations
from dataclasses import dataclass, field
from volvocarsapi.models import VolvoCarsApiBaseModel, VolvoCarsValue
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import API_NONE_VALUE
from .coordinator import VolvoBaseCoordinator, VolvoConfigEntry
from .entity import VolvoEntity, VolvoEntityDescription
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class VolvoBinarySensorDescription(
BinarySensorEntityDescription, VolvoEntityDescription
):
"""Describes a Volvo binary sensor entity."""
on_values: tuple[str, ...]
@dataclass(frozen=True, kw_only=True)
class VolvoCarsDoorDescription(VolvoBinarySensorDescription):
"""Describes a Volvo door entity."""
device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.DOOR
on_values: tuple[str, ...] = field(default=("OPEN", "AJAR"), init=False)
@dataclass(frozen=True, kw_only=True)
class VolvoCarsTireDescription(VolvoBinarySensorDescription):
"""Describes a Volvo tire entity."""
device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.PROBLEM
on_values: tuple[str, ...] = field(
default=("VERY_LOW_PRESSURE", "LOW_PRESSURE", "HIGH_PRESSURE"), init=False
)
@dataclass(frozen=True, kw_only=True)
class VolvoCarsWindowDescription(VolvoBinarySensorDescription):
"""Describes a Volvo window entity."""
device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.WINDOW
on_values: tuple[str, ...] = field(default=("OPEN", "AJAR"), init=False)
_DESCRIPTIONS: tuple[VolvoBinarySensorDescription, ...] = (
# brakes endpoint
VolvoBinarySensorDescription(
key="brake_fluid_level_warning",
api_field="brakeFluidLevelWarning",
device_class=BinarySensorDeviceClass.PROBLEM,
on_values=("TOO_LOW",),
),
# warnings endpoint
VolvoBinarySensorDescription(
key="brake_light_center_warning",
api_field="brakeLightCenterWarning",
device_class=BinarySensorDeviceClass.PROBLEM,
on_values=("FAILURE",),
entity_category=EntityCategory.DIAGNOSTIC,
),
# warnings endpoint
VolvoBinarySensorDescription(
key="brake_light_left_warning",
api_field="brakeLightLeftWarning",
device_class=BinarySensorDeviceClass.PROBLEM,
on_values=("FAILURE",),
entity_category=EntityCategory.DIAGNOSTIC,
),
# warnings endpoint
VolvoBinarySensorDescription(
key="brake_light_right_warning",
api_field="brakeLightRightWarning",
device_class=BinarySensorDeviceClass.PROBLEM,
on_values=("FAILURE",),
entity_category=EntityCategory.DIAGNOSTIC,
),
# engine endpoint
VolvoBinarySensorDescription(
key="coolant_level_warning",
api_field="engineCoolantLevelWarning",
device_class=BinarySensorDeviceClass.PROBLEM,
on_values=("TOO_LOW",),
),
# warnings endpoint
VolvoBinarySensorDescription(
key="daytime_running_light_left_warning",
api_field="daytimeRunningLightLeftWarning",
device_class=BinarySensorDeviceClass.PROBLEM,
on_values=("FAILURE",),
entity_category=EntityCategory.DIAGNOSTIC,
),
# warnings endpoint
VolvoBinarySensorDescription(
key="daytime_running_light_right_warning",
api_field="daytimeRunningLightRightWarning",
device_class=BinarySensorDeviceClass.PROBLEM,
on_values=("FAILURE",),
entity_category=EntityCategory.DIAGNOSTIC,
),
# doors endpoint
VolvoCarsDoorDescription(
key="door_front_left",
api_field="frontLeftDoor",
),
# doors endpoint
VolvoCarsDoorDescription(
key="door_front_right",
api_field="frontRightDoor",
),
# doors endpoint
VolvoCarsDoorDescription(
key="door_rear_left",
api_field="rearLeftDoor",
),
# doors endpoint
VolvoCarsDoorDescription(
key="door_rear_right",
api_field="rearRightDoor",
),
# engine-status endpoint
VolvoBinarySensorDescription(
key="engine_status",
api_field="engineStatus",
device_class=BinarySensorDeviceClass.RUNNING,
on_values=("RUNNING",),
),
# warnings endpoint
VolvoBinarySensorDescription(
key="fog_light_front_warning",
api_field="fogLightFrontWarning",
device_class=BinarySensorDeviceClass.PROBLEM,
on_values=("FAILURE",),
entity_category=EntityCategory.DIAGNOSTIC,
),
# warnings endpoint
VolvoBinarySensorDescription(
key="fog_light_rear_warning",
api_field="fogLightRearWarning",
device_class=BinarySensorDeviceClass.PROBLEM,
on_values=("FAILURE",),
entity_category=EntityCategory.DIAGNOSTIC,
),
# warnings endpoint
VolvoBinarySensorDescription(
key="hazard_lights_warning",
api_field="hazardLightsWarning",
device_class=BinarySensorDeviceClass.PROBLEM,
on_values=("FAILURE",),
entity_category=EntityCategory.DIAGNOSTIC,
),
# warnings endpoint
VolvoBinarySensorDescription(
key="high_beam_left_warning",
api_field="highBeamLeftWarning",
device_class=BinarySensorDeviceClass.PROBLEM,
on_values=("FAILURE",),
entity_category=EntityCategory.DIAGNOSTIC,
),
# warnings endpoint
VolvoBinarySensorDescription(
key="high_beam_right_warning",
api_field="highBeamRightWarning",
device_class=BinarySensorDeviceClass.PROBLEM,
on_values=("FAILURE",),
entity_category=EntityCategory.DIAGNOSTIC,
),
# doors endpoint
VolvoCarsDoorDescription(
key="hood",
api_field="hood",
),
# warnings endpoint
VolvoBinarySensorDescription(
key="low_beam_left_warning",
api_field="lowBeamLeftWarning",
device_class=BinarySensorDeviceClass.PROBLEM,
on_values=("FAILURE",),
entity_category=EntityCategory.DIAGNOSTIC,
),
# warnings endpoint
VolvoBinarySensorDescription(
key="low_beam_right_warning",
api_field="lowBeamRightWarning",
device_class=BinarySensorDeviceClass.PROBLEM,
on_values=("FAILURE",),
entity_category=EntityCategory.DIAGNOSTIC,
),
# engine endpoint
VolvoBinarySensorDescription(
key="oil_level_warning",
api_field="oilLevelWarning",
device_class=BinarySensorDeviceClass.PROBLEM,
on_values=("SERVICE_REQUIRED", "TOO_LOW", "TOO_HIGH"),
),
# position lights
VolvoBinarySensorDescription(
key="position_light_front_left_warning",
api_field="positionLightFrontLeftWarning",
device_class=BinarySensorDeviceClass.PROBLEM,
on_values=("FAILURE",),
entity_category=EntityCategory.DIAGNOSTIC,
),
# position lights
VolvoBinarySensorDescription(
key="position_light_front_right_warning",
api_field="positionLightFrontRightWarning",
device_class=BinarySensorDeviceClass.PROBLEM,
on_values=("FAILURE",),
entity_category=EntityCategory.DIAGNOSTIC,
),
# position lights
VolvoBinarySensorDescription(
key="position_light_rear_left_warning",
api_field="positionLightRearLeftWarning",
device_class=BinarySensorDeviceClass.PROBLEM,
on_values=("FAILURE",),
entity_category=EntityCategory.DIAGNOSTIC,
),
# position lights
VolvoBinarySensorDescription(
key="position_light_rear_right_warning",
api_field="positionLightRearRightWarning",
device_class=BinarySensorDeviceClass.PROBLEM,
on_values=("FAILURE",),
entity_category=EntityCategory.DIAGNOSTIC,
),
# registration plate light
VolvoBinarySensorDescription(
key="registration_plate_light_warning",
api_field="registrationPlateLightWarning",
device_class=BinarySensorDeviceClass.PROBLEM,
on_values=("FAILURE",),
entity_category=EntityCategory.DIAGNOSTIC,
),
# reverse lights
VolvoBinarySensorDescription(
key="reverse_lights_warning",
api_field="reverseLightsWarning",
device_class=BinarySensorDeviceClass.PROBLEM,
on_values=("FAILURE",),
entity_category=EntityCategory.DIAGNOSTIC,
),
# warnings endpoint
VolvoBinarySensorDescription(
key="side_mark_lights_warning",
api_field="sideMarkLightsWarning",
device_class=BinarySensorDeviceClass.PROBLEM,
on_values=("FAILURE",),
entity_category=EntityCategory.DIAGNOSTIC,
),
# windows endpoint
VolvoCarsWindowDescription(
key="sunroof",
api_field="sunroof",
),
# tyres endpoint
VolvoCarsTireDescription(
key="tire_front_left",
api_field="frontLeft",
),
# tyres endpoint
VolvoCarsTireDescription(
key="tire_front_right",
api_field="frontRight",
),
# tyres endpoint
VolvoCarsTireDescription(
key="tire_rear_left",
api_field="rearLeft",
),
# tyres endpoint
VolvoCarsTireDescription(
key="tire_rear_right",
api_field="rearRight",
),
# doors endpoint
VolvoCarsDoorDescription(
key="tailgate",
api_field="tailgate",
),
# doors endpoint
VolvoCarsDoorDescription(
key="tank_lid",
api_field="tankLid",
),
# warnings endpoint
VolvoBinarySensorDescription(
key="turn_indication_front_left_warning",
api_field="turnIndicationFrontLeftWarning",
device_class=BinarySensorDeviceClass.PROBLEM,
on_values=("FAILURE",),
entity_category=EntityCategory.DIAGNOSTIC,
),
# warnings endpoint
VolvoBinarySensorDescription(
key="turn_indication_front_right_warning",
api_field="turnIndicationFrontRightWarning",
device_class=BinarySensorDeviceClass.PROBLEM,
on_values=("FAILURE",),
entity_category=EntityCategory.DIAGNOSTIC,
),
# warnings endpoint
VolvoBinarySensorDescription(
key="turn_indication_rear_left_warning",
api_field="turnIndicationRearLeftWarning",
device_class=BinarySensorDeviceClass.PROBLEM,
on_values=("FAILURE",),
entity_category=EntityCategory.DIAGNOSTIC,
),
# warnings endpoint
VolvoBinarySensorDescription(
key="turn_indication_rear_right_warning",
api_field="turnIndicationRearRightWarning",
device_class=BinarySensorDeviceClass.PROBLEM,
on_values=("FAILURE",),
entity_category=EntityCategory.DIAGNOSTIC,
),
# diagnostics endpoint
VolvoBinarySensorDescription(
key="washer_fluid_level_warning",
api_field="washerFluidLevelWarning",
device_class=BinarySensorDeviceClass.PROBLEM,
on_values=("TOO_LOW",),
),
# windows endpoint
VolvoCarsWindowDescription(
key="window_front_left",
api_field="frontLeftWindow",
),
# windows endpoint
VolvoCarsWindowDescription(
key="window_front_right",
api_field="frontRightWindow",
),
# windows endpoint
VolvoCarsWindowDescription(
key="window_rear_left",
api_field="rearLeftWindow",
),
# windows endpoint
VolvoCarsWindowDescription(
key="window_rear_right",
api_field="rearRightWindow",
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: VolvoConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up binary sensors."""
coordinators = entry.runtime_data
async_add_entities(
VolvoBinarySensor(coordinator, description)
for coordinator in coordinators
for description in _DESCRIPTIONS
if description.api_field in coordinator.data
)
class VolvoBinarySensor(VolvoEntity, BinarySensorEntity):
"""Volvo binary sensor."""
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:
self._attr_is_on = None
return
assert isinstance(api_field, VolvoCarsValue)
assert isinstance(api_field.value, str)
value = api_field.value
self._attr_is_on = (
value in self.entity_description.on_values
if value.upper() != API_NONE_VALUE
else None
)

View File

@@ -3,12 +3,9 @@
from homeassistant.const import Platform from homeassistant.const import Platform
DOMAIN = "volvo" DOMAIN = "volvo"
PLATFORMS: list[Platform] = [Platform.SENSOR] PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR]
ATTR_API_TIMESTAMP = "api_timestamp"
API_NONE_VALUE = "UNSPECIFIED"
CONF_VIN = "vin" CONF_VIN = "vin"
DATA_BATTERY_CAPACITY = "battery_capacity_kwh" DATA_BATTERY_CAPACITY = "battery_capacity_kwh"
MANUFACTURER = "Volvo" MANUFACTURER = "Volvo"

View File

@@ -29,6 +29,7 @@ from .const import DATA_BATTERY_CAPACITY, DOMAIN
VERY_SLOW_INTERVAL = 60 VERY_SLOW_INTERVAL = 60
SLOW_INTERVAL = 15 SLOW_INTERVAL = 15
MEDIUM_INTERVAL = 2 MEDIUM_INTERVAL = 2
FAST_INTERVAL = 1
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -187,9 +188,13 @@ class VolvoVerySlowIntervalCoordinator(VolvoBaseCoordinator):
self, self,
) -> list[Callable[[], Coroutine[Any, Any, Any]]]: ) -> list[Callable[[], Coroutine[Any, Any, Any]]]:
return [ return [
self.api.async_get_brakes_status,
self.api.async_get_diagnostics, self.api.async_get_diagnostics,
self.api.async_get_engine_warnings,
self.api.async_get_odometer, self.api.async_get_odometer,
self.api.async_get_statistics, self.api.async_get_statistics,
self.api.async_get_tyre_states,
self.api.async_get_warnings,
] ]
async def _async_update_data(self) -> CoordinatorData: async def _async_update_data(self) -> CoordinatorData:
@@ -265,6 +270,8 @@ class VolvoMediumIntervalCoordinator(VolvoBaseCoordinator):
async def _async_determine_api_calls( async def _async_determine_api_calls(
self, self,
) -> list[Callable[[], Coroutine[Any, Any, Any]]]: ) -> list[Callable[[], Coroutine[Any, Any, Any]]]:
api_calls: list[Any] = []
if self.vehicle.has_battery_engine(): if self.vehicle.has_battery_engine():
capabilities = await self.api.async_get_energy_capabilities() capabilities = await self.api.async_get_energy_capabilities()
@@ -279,9 +286,12 @@ class VolvoMediumIntervalCoordinator(VolvoBaseCoordinator):
if isinstance(value, dict) and value.get("isSupported", False) if isinstance(value, dict) and value.get("isSupported", False)
] ]
return [self._async_get_energy_state] api_calls.append(self._async_get_energy_state)
return [] if self.vehicle.has_combustion_engine():
api_calls.append(self.api.async_get_engine_status)
return api_calls
async def _async_get_energy_state( async def _async_get_energy_state(
self, self,
@@ -301,3 +311,33 @@ class VolvoMediumIntervalCoordinator(VolvoBaseCoordinator):
for key, value in energy_state.items() for key, value in energy_state.items()
if key in self._supported_capabilities if key in self._supported_capabilities
} }
class VolvoFastIntervalCoordinator(VolvoBaseCoordinator):
"""Volvo coordinator with fast update rate."""
def __init__(
self,
hass: HomeAssistant,
entry: VolvoConfigEntry,
api: VolvoCarsApi,
vehicle: VolvoCarsVehicle,
) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
entry,
api,
vehicle,
timedelta(minutes=FAST_INTERVAL),
"Volvo fast interval coordinator",
)
async def _async_determine_api_calls(
self,
) -> list[Callable[[], Coroutine[Any, Any, Any]]]:
return [
self.api.async_get_doors_status,
self.api.async_get_window_states,
]

View File

@@ -1,5 +1,265 @@
{ {
"entity": { "entity": {
"binary_sensor": {
"brake_fluid_level_warning": {
"default": "mdi:car-brake-fluid-level",
"state": {
"on": "mdi:car-brake-alert"
}
},
"brake_light_center_warning": {
"default": "mdi:car-light-dimmed",
"state": {
"on": "mdi:car-light-alert"
}
},
"brake_light_left_warning": {
"default": "mdi:car-light-dimmed",
"state": {
"on": "mdi:car-light-alert"
}
},
"brake_light_right_warning": {
"default": "mdi:car-light-dimmed",
"state": {
"on": "mdi:car-light-alert"
}
},
"coolant_level_warning": {
"default": "mdi:car-coolant-level"
},
"daytime_running_light_left_warning": {
"default": "mdi:car-light-dimmed",
"state": {
"on": "mdi:car-light-alert"
}
},
"daytime_running_light_right_warning": {
"default": "mdi:car-light-dimmed",
"state": {
"on": "mdi:car-light-alert"
}
},
"door_front_left": {
"default": "mdi:car-door-lock",
"state": {
"on": "mdi:car-door-lock-open"
}
},
"door_front_right": {
"default": "mdi:car-door-lock",
"state": {
"on": "mdi:car-door-lock-open"
}
},
"door_rear_left": {
"default": "mdi:car-door-lock",
"state": {
"on": "mdi:car-door-lock-open"
}
},
"door_rear_right": {
"default": "mdi:car-door-lock",
"state": {
"on": "mdi:car-door-lock-open"
}
},
"engine_status": {
"default": "mdi:engine-off",
"state": {
"on": "mdi:engine"
}
},
"fog_light_front_warning": {
"default": "mdi:car-light-fog",
"state": {
"on": "mdi:car-light-alert"
}
},
"fog_light_rear_warning": {
"default": "mdi:car-light-fog",
"state": {
"on": "mdi:car-light-alert"
}
},
"hazard_lights_warning": {
"default": "mdi:hazard-lights",
"state": {
"on": "mdi:car-light-alert"
}
},
"high_beam_left_warning": {
"default": "mdi:car-light-high",
"state": {
"on": "mdi:car-light-alert"
}
},
"high_beam_right_warning": {
"default": "mdi:car-light-high",
"state": {
"on": "mdi:car-light-alert"
}
},
"hood": {
"default": "mdi:car-door-lock",
"state": {
"on": "mdi:car-door-lock-open"
}
},
"low_beam_left_warning": {
"default": "mdi:car-light-dimmed",
"state": {
"on": "mdi:car-light-alert"
}
},
"low_beam_right_warning": {
"default": "mdi:car-light-dimmed",
"state": {
"on": "mdi:car-light-alert"
}
},
"oil_level_warning": {
"default": "mdi:oil-level"
},
"position_light_front_left_warning": {
"default": "mdi:car-parking-lights",
"state": {
"on": "mdi:car-light-alert"
}
},
"position_light_front_right_warning": {
"default": "mdi:car-parking-lights",
"state": {
"on": "mdi:car-light-alert"
}
},
"position_light_rear_left_warning": {
"default": "mdi:car-parking-lights",
"state": {
"on": "mdi:car-light-alert"
}
},
"position_light_rear_right_warning": {
"default": "mdi:car-parking-lights",
"state": {
"on": "mdi:car-light-alert"
}
},
"registration_plate_light_warning": {
"default": "mdi:lightbulb-outline",
"state": {
"on": "mdi:lightbulb-off-outline"
}
},
"reverse_lights_warning": {
"default": "mdi:car-light-dimmed",
"state": {
"on": "mdi:car-light-alert"
}
},
"side_mark_lights_warning": {
"default": "mdi:wall-sconce-round",
"state": {
"on": "mdi:car-light-alert"
}
},
"sunroof": {
"default": "mdi:car-door-lock",
"state": {
"on": "mdi:car-door-lock-open"
}
},
"tailgate": {
"default": "mdi:car-door-lock",
"state": {
"on": "mdi:car-door-lock-open"
}
},
"tank_lid": {
"default": "mdi:car-door-lock",
"state": {
"on": "mdi:car-door-lock-open"
}
},
"turn_indication_front_left_warning": {
"default": "mdi:arrow-left-top",
"state": {
"on": "mdi:car-light-alert"
}
},
"turn_indication_front_right_warning": {
"default": "mdi:arrow-right-top",
"state": {
"on": "mdi:car-light-alert"
}
},
"turn_indication_rear_left_warning": {
"default": "mdi:arrow-left-top",
"state": {
"on": "mdi:car-light-alert"
}
},
"turn_indication_rear_right_warning": {
"default": "mdi:arrow-right-top",
"state": {
"on": "mdi:car-light-alert"
}
},
"tire_front_left": {
"default": "mdi:tire",
"state": {
"on": "mdi:car-tire-alert"
}
},
"tire_front_right": {
"default": "mdi:tire",
"state": {
"on": "mdi:car-tire-alert"
}
},
"tire_rear_left": {
"default": "mdi:tire",
"state": {
"on": "mdi:car-tire-alert"
}
},
"tire_rear_right": {
"default": "mdi:tire",
"state": {
"on": "mdi:car-tire-alert"
}
},
"washer_fluid_level_warning": {
"default": "mdi:wiper-wash",
"state": {
"on": "mdi:wiper-wash-alert"
}
},
"window_front_left": {
"default": "mdi:car-door-lock",
"state": {
"on": "mdi:car-door-lock-open"
}
},
"window_front_right": {
"default": "mdi:car-door-lock",
"state": {
"on": "mdi:car-door-lock-open"
}
},
"window_rear_left": {
"default": "mdi:car-door-lock",
"state": {
"on": "mdi:car-door-lock-open"
}
},
"window_rear_right": {
"default": "mdi:car-door-lock",
"state": {
"on": "mdi:car-door-lock-open"
}
}
},
"sensor": { "sensor": {
"availability": { "availability": {
"default": "mdi:car-connected" "default": "mdi:car-connected"

View File

@@ -35,8 +35,8 @@ 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 .const import DATA_BATTERY_CAPACITY from .const import API_NONE_VALUE, DATA_BATTERY_CAPACITY
from .coordinator import VolvoBaseCoordinator, VolvoConfigEntry from .coordinator import VolvoConfigEntry
from .entity import VolvoEntity, VolvoEntityDescription, value_to_translation_key from .entity import VolvoEntity, VolvoEntityDescription, value_to_translation_key
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
@@ -248,7 +248,7 @@ _DESCRIPTIONS: tuple[VolvoSensorDescription, ...] = (
# 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`.
# In issue #150213, a user described to behavior as follows: # In issue #150213, a user described the behavior as follows:
# - For a `distanceToEmptyBattery` of 250km, the `electricRange` was 150mi # - For a `distanceToEmptyBattery` of 250km, the `electricRange` was 150mi
# - For a `distanceToEmptyBattery` of 260km, the `electricRange` was 160mi # - For a `distanceToEmptyBattery` of 260km, the `electricRange` was 160mi
VolvoSensorDescription( VolvoSensorDescription(
@@ -354,27 +354,13 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up sensors.""" """Set up sensors."""
entities: list[VolvoSensor] = []
added_keys: set[str] = set()
def _add_entity(
coordinator: VolvoBaseCoordinator, description: VolvoSensorDescription
) -> None:
entities.append(VolvoSensor(coordinator, description))
added_keys.add(description.key)
coordinators = entry.runtime_data coordinators = entry.runtime_data
async_add_entities(
for coordinator in coordinators: VolvoSensor(coordinator, description)
for description in _DESCRIPTIONS: for coordinator in coordinators
if description.key in added_keys: for description in _DESCRIPTIONS
continue if description.api_field in coordinator.data
)
if description.api_field in coordinator.data:
_add_entity(coordinator, description)
async_add_entities(entities)
class VolvoSensor(VolvoEntity, SensorEntity): class VolvoSensor(VolvoEntity, SensorEntity):
@@ -401,7 +387,7 @@ class VolvoSensor(VolvoEntity, SensorEntity):
native_value = str(native_value) native_value = str(native_value)
native_value = ( native_value = (
value_to_translation_key(native_value) value_to_translation_key(native_value)
if native_value.upper() != "UNSPECIFIED" if native_value.upper() != API_NONE_VALUE
else None else None
) )

View File

@@ -1,4 +1,7 @@
{ {
"common": {
"pressure": "Pressure"
},
"config": { "config": {
"step": { "step": {
"pick_implementation": { "pick_implementation": {
@@ -50,6 +53,140 @@
} }
}, },
"entity": { "entity": {
"binary_sensor": {
"brake_fluid_level_warning": {
"name": "Brake fluid"
},
"brake_light_center_warning": {
"name": "Brake light center"
},
"brake_light_left_warning": {
"name": "Brake light left"
},
"brake_light_right_warning": {
"name": "Brake light right"
},
"coolant_level_warning": {
"name": "Coolant level"
},
"daytime_running_light_left_warning": {
"name": "Daytime running light left"
},
"daytime_running_light_right_warning": {
"name": "Daytime running light right"
},
"door_front_left": {
"name": "Door front left"
},
"door_front_right": {
"name": "Door front right"
},
"door_rear_left": {
"name": "Door rear left"
},
"door_rear_right": {
"name": "Door rear right"
},
"engine_status": {
"name": "Engine status"
},
"fog_light_front_warning": {
"name": "Fog light front"
},
"fog_light_rear_warning": {
"name": "Fog light rear"
},
"hazard_lights_warning": {
"name": "Hazard lights"
},
"high_beam_left_warning": {
"name": "High beam left"
},
"high_beam_right_warning": {
"name": "High beam right"
},
"hood": {
"name": "Hood"
},
"low_beam_left_warning": {
"name": "Low beam left"
},
"low_beam_right_warning": {
"name": "Low beam right"
},
"oil_level_warning": {
"name": "Oil level"
},
"position_light_front_left_warning": {
"name": "Position light front left"
},
"position_light_front_right_warning": {
"name": "Position light front right"
},
"position_light_rear_left_warning": {
"name": "Position light rear left"
},
"position_light_rear_right_warning": {
"name": "Position light rear right"
},
"registration_plate_light_warning": {
"name": "Registration plate light"
},
"reverse_lights_warning": {
"name": "Reverse lights"
},
"side_mark_lights_warning": {
"name": "Side mark lights"
},
"sunroof": {
"name": "Sunroof"
},
"tailgate": {
"name": "Tailgate"
},
"tank_lid": {
"name": "Tank lid"
},
"turn_indication_front_left_warning": {
"name": "Turn indication front left"
},
"turn_indication_front_right_warning": {
"name": "Turn indication front right"
},
"turn_indication_rear_left_warning": {
"name": "Turn indication rear left"
},
"turn_indication_rear_right_warning": {
"name": "Turn indication rear right"
},
"tire_front_left": {
"name": "Tire front left"
},
"tire_front_right": {
"name": "Tire front right"
},
"tire_rear_left": {
"name": "Tire rear left"
},
"tire_rear_right": {
"name": "Tire rear right"
},
"washer_fluid_level_warning": {
"name": "Washer fluid"
},
"window_front_left": {
"name": "Window front left"
},
"window_front_right": {
"name": "Window front right"
},
"window_rear_left": {
"name": "Window rear left"
},
"window_rear_right": {
"name": "Window rear right"
}
},
"sensor": { "sensor": {
"availability": { "availability": {
"name": "Car connection", "name": "Car connection",

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,32 @@
"""Test Volvo binary sensors."""
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.parametrize(
"full_model",
["ex30_2024", "s90_diesel_2018", "xc40_electric_2024", "xc90_petrol_2019"],
)
async def test_binary_sensor(
hass: HomeAssistant,
setup_integration: Callable[[], Awaitable[bool]],
entity_registry: er.EntityRegistry,
snapshot: SnapshotAssertion,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test binary sensor."""
with patch("homeassistant.components.volvo.PLATFORMS", [Platform.BINARY_SENSOR]):
assert await setup_integration()
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)