From 9531ae10f28e8d22aca3ed68a8ab6602eaa7a85e Mon Sep 17 00:00:00 2001 From: Stephan van Rooij <1292510+svrooij@users.noreply.github.com> Date: Sat, 20 Sep 2025 14:08:53 +0200 Subject: [PATCH] Remove volvooncall (#150725) Co-authored-by: G Johansson --- CODEOWNERS | 4 +- .../components/volvooncall/__init__.py | 83 +++----- .../components/volvooncall/binary_sensor.py | 79 -------- .../components/volvooncall/config_flow.py | 116 +----------- homeassistant/components/volvooncall/const.py | 65 +------ .../components/volvooncall/coordinator.py | 40 ---- .../components/volvooncall/device_tracker.py | 72 ------- .../components/volvooncall/entity.py | 88 --------- .../components/volvooncall/errors.py | 7 - homeassistant/components/volvooncall/lock.py | 80 -------- .../components/volvooncall/manifest.json | 5 +- .../components/volvooncall/models.py | 100 ---------- .../components/volvooncall/sensor.py | 72 ------- .../components/volvooncall/strings.json | 21 +- .../components/volvooncall/switch.py | 78 -------- requirements_all.txt | 3 - requirements_test_all.txt | 3 - .../volvooncall/test_config_flow.py | 179 ++---------------- tests/components/volvooncall/test_init.py | 76 ++++++++ 19 files changed, 138 insertions(+), 1033 deletions(-) delete mode 100644 homeassistant/components/volvooncall/binary_sensor.py delete mode 100644 homeassistant/components/volvooncall/coordinator.py delete mode 100644 homeassistant/components/volvooncall/device_tracker.py delete mode 100644 homeassistant/components/volvooncall/entity.py delete mode 100644 homeassistant/components/volvooncall/errors.py delete mode 100644 homeassistant/components/volvooncall/lock.py delete mode 100644 homeassistant/components/volvooncall/models.py delete mode 100644 homeassistant/components/volvooncall/sensor.py delete mode 100644 homeassistant/components/volvooncall/switch.py create mode 100644 tests/components/volvooncall/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index 543ef798b1c..a0f5171dd49 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1729,8 +1729,8 @@ build.json @home-assistant/supervisor /tests/components/volumio/ @OnFreund /homeassistant/components/volvo/ @thomasddn /tests/components/volvo/ @thomasddn -/homeassistant/components/volvooncall/ @molobrakos -/tests/components/volvooncall/ @molobrakos +/homeassistant/components/volvooncall/ @molobrakos @svrooij +/tests/components/volvooncall/ @molobrakos @svrooij /homeassistant/components/wake_on_lan/ @ntilley905 /tests/components/wake_on_lan/ @ntilley905 /homeassistant/components/wake_word/ @home-assistant/core @synesthesiam diff --git a/homeassistant/components/volvooncall/__init__.py b/homeassistant/components/volvooncall/__init__.py index 1a53f9a5dc4..6542f34b487 100644 --- a/homeassistant/components/volvooncall/__init__.py +++ b/homeassistant/components/volvooncall/__init__.py @@ -1,71 +1,46 @@ -"""Support for Volvo On Call.""" +"""The Volvo On Call integration.""" -from volvooncall import Connection +from __future__ import annotations from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_PASSWORD, - CONF_REGION, - CONF_UNIT_SYSTEM, - CONF_USERNAME, -) from homeassistant.core import HomeAssistant -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers import issue_registry as ir -from .const import ( - CONF_SCANDINAVIAN_MILES, - DOMAIN, - PLATFORMS, - UNIT_SYSTEM_METRIC, - UNIT_SYSTEM_SCANDINAVIAN_MILES, -) -from .coordinator import VolvoUpdateCoordinator -from .models import VolvoData +from .const import DOMAIN async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up the Volvo On Call component from a ConfigEntry.""" + """Set up Volvo On Call integration.""" - # added CONF_UNIT_SYSTEM / deprecated CONF_SCANDINAVIAN_MILES in 2022.10 to support imperial units - if CONF_UNIT_SYSTEM not in entry.data: - new_conf = {**entry.data} - - scandinavian_miles: bool = entry.data[CONF_SCANDINAVIAN_MILES] - - new_conf[CONF_UNIT_SYSTEM] = ( - UNIT_SYSTEM_SCANDINAVIAN_MILES if scandinavian_miles else UNIT_SYSTEM_METRIC - ) - - hass.config_entries.async_update_entry(entry, data=new_conf) - - session = async_get_clientsession(hass) - - connection = Connection( - session=session, - username=entry.data[CONF_USERNAME], - password=entry.data[CONF_PASSWORD], - service_url=None, - region=entry.data[CONF_REGION], + # Create repair issue pointing to the new volvo integration + ir.async_create_issue( + hass, + DOMAIN, + "volvooncall_deprecated", + breaks_in_ha_version="2026.3", + is_fixable=False, + severity=ir.IssueSeverity.WARNING, + translation_key="volvooncall_deprecated", ) - hass.data.setdefault(DOMAIN, {}) - - volvo_data = VolvoData(hass, connection, entry) - - coordinator = VolvoUpdateCoordinator(hass, entry, volvo_data) - - await coordinator.async_config_entry_first_refresh() - - hass.data[DOMAIN][entry.entry_id] = coordinator - - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): - hass.data[DOMAIN].pop(entry.entry_id) - return unload_ok + # Only delete the repair issue if this is the last config entry for this domain + remaining_entries = [ + config_entry + for config_entry in hass.config_entries.async_entries(DOMAIN) + if config_entry.entry_id != entry.entry_id + ] + + if not remaining_entries: + ir.async_delete_issue( + hass, + DOMAIN, + "volvooncall_deprecated", + ) + + return True diff --git a/homeassistant/components/volvooncall/binary_sensor.py b/homeassistant/components/volvooncall/binary_sensor.py deleted file mode 100644 index 2ba8d19e3db..00000000000 --- a/homeassistant/components/volvooncall/binary_sensor.py +++ /dev/null @@ -1,79 +0,0 @@ -"""Support for VOC.""" - -from __future__ import annotations - -from contextlib import suppress - -import voluptuous as vol -from volvooncall.dashboard import Instrument - -from homeassistant.components.binary_sensor import ( - DEVICE_CLASSES_SCHEMA, - BinarySensorEntity, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback - -from .const import DOMAIN, VOLVO_DISCOVERY_NEW -from .coordinator import VolvoUpdateCoordinator -from .entity import VolvoEntity - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddConfigEntryEntitiesCallback, -) -> None: - """Configure binary_sensors from a config entry created in the integrations UI.""" - coordinator: VolvoUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - volvo_data = coordinator.volvo_data - - @callback - def async_discover_device(instruments: list[Instrument]) -> None: - """Discover and add a discovered Volvo On Call binary sensor.""" - async_add_entities( - VolvoSensor( - coordinator, - instrument.vehicle.vin, - instrument.component, - instrument.attr, - instrument.slug_attr, - ) - for instrument in instruments - if instrument.component == "binary_sensor" - ) - - async_discover_device([*volvo_data.instruments]) - - config_entry.async_on_unload( - async_dispatcher_connect(hass, VOLVO_DISCOVERY_NEW, async_discover_device) - ) - - -class VolvoSensor(VolvoEntity, BinarySensorEntity): - """Representation of a Volvo sensor.""" - - def __init__( - self, - coordinator: VolvoUpdateCoordinator, - vin: str, - component: str, - attribute: str, - slug_attr: str, - ) -> None: - """Initialize the sensor.""" - super().__init__(vin, component, attribute, slug_attr, coordinator) - - with suppress(vol.Invalid): - self._attr_device_class = DEVICE_CLASSES_SCHEMA( - self.instrument.device_class - ) - - @property - def is_on(self) -> bool | None: - """Fetch from update coordinator.""" - if self.instrument.attr == "is_locked": - return not self.instrument.is_on - return self.instrument.is_on diff --git a/homeassistant/components/volvooncall/config_flow.py b/homeassistant/components/volvooncall/config_flow.py index ccb0a7f62e1..e1aa95cb730 100644 --- a/homeassistant/components/volvooncall/config_flow.py +++ b/homeassistant/components/volvooncall/config_flow.py @@ -2,127 +2,21 @@ from __future__ import annotations -from collections.abc import Mapping -import logging from typing import Any -import voluptuous as vol -from volvooncall import Connection +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult -from homeassistant.const import ( - CONF_PASSWORD, - CONF_REGION, - CONF_UNIT_SYSTEM, - CONF_USERNAME, -) -from homeassistant.helpers.aiohttp_client import async_get_clientsession - -from .const import ( - CONF_MUTABLE, - DOMAIN, - UNIT_SYSTEM_IMPERIAL, - UNIT_SYSTEM_METRIC, - UNIT_SYSTEM_SCANDINAVIAN_MILES, -) -from .errors import InvalidAuth -from .models import VolvoData - -_LOGGER = logging.getLogger(__name__) +from .const import DOMAIN class VolvoOnCallConfigFlow(ConfigFlow, domain=DOMAIN): - """VolvoOnCall config flow.""" + """Handle a config flow for Volvo On Call.""" VERSION = 1 async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: - """Handle user step.""" - errors = {} - defaults = { - CONF_USERNAME: "", - CONF_PASSWORD: "", - CONF_REGION: None, - CONF_MUTABLE: True, - CONF_UNIT_SYSTEM: UNIT_SYSTEM_METRIC, - } + """Handle the initial step.""" - if user_input is not None: - await self.async_set_unique_id(user_input[CONF_USERNAME]) - - if self.source != SOURCE_REAUTH: - self._abort_if_unique_id_configured() - - try: - await self.is_valid(user_input) - except InvalidAuth: - errors["base"] = "invalid_auth" - except Exception: - _LOGGER.exception("Unhandled exception in user step") - errors["base"] = "unknown" - if not errors: - if self.source == SOURCE_REAUTH: - return self.async_update_reload_and_abort( - self._get_reauth_entry(), data_updates=user_input - ) - - return self.async_create_entry( - title=user_input[CONF_USERNAME], data=user_input - ) - elif self.source == SOURCE_REAUTH: - reauth_entry = self._get_reauth_entry() - for key in defaults: - defaults[key] = reauth_entry.data.get(key) - - user_schema = vol.Schema( - { - vol.Required(CONF_USERNAME, default=defaults[CONF_USERNAME]): str, - vol.Required(CONF_PASSWORD, default=defaults[CONF_PASSWORD]): str, - vol.Required(CONF_REGION, default=defaults[CONF_REGION]): vol.In( - {"na": "North America", "cn": "China", None: "Rest of world"} - ), - vol.Optional( - CONF_UNIT_SYSTEM, default=defaults[CONF_UNIT_SYSTEM] - ): vol.In( - { - UNIT_SYSTEM_METRIC: "Metric", - UNIT_SYSTEM_SCANDINAVIAN_MILES: ( - "Metric with Scandinavian Miles" - ), - UNIT_SYSTEM_IMPERIAL: "Imperial", - } - ), - vol.Optional(CONF_MUTABLE, default=defaults[CONF_MUTABLE]): bool, - }, - ) - - return self.async_show_form( - step_id="user", data_schema=user_schema, errors=errors - ) - - async def async_step_reauth( - self, entry_data: Mapping[str, Any] - ) -> ConfigFlowResult: - """Perform reauth upon an API authentication error.""" - return await self.async_step_user() - - async def is_valid(self, user_input): - """Check for user input errors.""" - - session = async_get_clientsession(self.hass) - - region: str | None = user_input.get(CONF_REGION) - - connection = Connection( - session=session, - username=user_input[CONF_USERNAME], - password=user_input[CONF_PASSWORD], - service_url=None, - region=region, - ) - - test_volvo_data = VolvoData(self.hass, connection, user_input) - - await test_volvo_data.auth_is_valid() + return self.async_abort(reason="deprecated") diff --git a/homeassistant/components/volvooncall/const.py b/homeassistant/components/volvooncall/const.py index 4c969669af6..e04de08008b 100644 --- a/homeassistant/components/volvooncall/const.py +++ b/homeassistant/components/volvooncall/const.py @@ -1,66 +1,3 @@ -"""Constants for volvooncall.""" - -from datetime import timedelta +"""Constants for the Volvo On Call integration.""" DOMAIN = "volvooncall" - -DEFAULT_UPDATE_INTERVAL = timedelta(minutes=1) - -CONF_SERVICE_URL = "service_url" -CONF_SCANDINAVIAN_MILES = "scandinavian_miles" -CONF_MUTABLE = "mutable" - -UNIT_SYSTEM_SCANDINAVIAN_MILES = "scandinavian_miles" -UNIT_SYSTEM_METRIC = "metric" -UNIT_SYSTEM_IMPERIAL = "imperial" - -PLATFORMS = { - "sensor": "sensor", - "binary_sensor": "binary_sensor", - "lock": "lock", - "device_tracker": "device_tracker", - "switch": "switch", -} - -RESOURCES = [ - "position", - "lock", - "heater", - "odometer", - "trip_meter1", - "trip_meter2", - "average_speed", - "fuel_amount", - "fuel_amount_level", - "average_fuel_consumption", - "distance_to_empty", - "washer_fluid_level", - "brake_fluid", - "service_warning_status", - "bulb_failures", - "battery_range", - "battery_level", - "time_to_fully_charged", - "battery_charge_status", - "engine_start", - "last_trip", - "is_engine_running", - "doors_hood_open", - "doors_tailgate_open", - "doors_front_left_door_open", - "doors_front_right_door_open", - "doors_rear_left_door_open", - "doors_rear_right_door_open", - "windows_front_left_window_open", - "windows_front_right_window_open", - "windows_rear_left_window_open", - "windows_rear_right_window_open", - "tyre_pressure_front_left_tyre_pressure", - "tyre_pressure_front_right_tyre_pressure", - "tyre_pressure_rear_left_tyre_pressure", - "tyre_pressure_rear_right_tyre_pressure", - "any_door_open", - "any_window_open", -] - -VOLVO_DISCOVERY_NEW = "volvo_discovery_new" diff --git a/homeassistant/components/volvooncall/coordinator.py b/homeassistant/components/volvooncall/coordinator.py deleted file mode 100644 index 2c3e2ba365f..00000000000 --- a/homeassistant/components/volvooncall/coordinator.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Support for Volvo On Call.""" - -import asyncio -import logging - -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator - -from .const import DEFAULT_UPDATE_INTERVAL -from .models import VolvoData - -_LOGGER = logging.getLogger(__name__) - - -class VolvoUpdateCoordinator(DataUpdateCoordinator[None]): - """Volvo coordinator.""" - - config_entry: ConfigEntry - - def __init__( - self, hass: HomeAssistant, config_entry: ConfigEntry, volvo_data: VolvoData - ) -> None: - """Initialize the data update coordinator.""" - - super().__init__( - hass, - _LOGGER, - config_entry=config_entry, - name="volvooncall", - update_interval=DEFAULT_UPDATE_INTERVAL, - ) - - self.volvo_data = volvo_data - - async def _async_update_data(self) -> None: - """Fetch data from API endpoint.""" - - async with asyncio.timeout(10): - await self.volvo_data.update() diff --git a/homeassistant/components/volvooncall/device_tracker.py b/homeassistant/components/volvooncall/device_tracker.py deleted file mode 100644 index 018acb02d49..00000000000 --- a/homeassistant/components/volvooncall/device_tracker.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Support for tracking a Volvo.""" - -from __future__ import annotations - -from volvooncall.dashboard import Instrument - -from homeassistant.components.device_tracker import TrackerEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback - -from .const import DOMAIN, VOLVO_DISCOVERY_NEW -from .coordinator import VolvoUpdateCoordinator -from .entity import VolvoEntity - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddConfigEntryEntitiesCallback, -) -> None: - """Configure device_trackers from a config entry created in the integrations UI.""" - coordinator: VolvoUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - volvo_data = coordinator.volvo_data - - @callback - def async_discover_device(instruments: list[Instrument]) -> None: - """Discover and add a discovered Volvo On Call device tracker.""" - async_add_entities( - VolvoTrackerEntity( - instrument.vehicle.vin, - instrument.component, - instrument.attr, - instrument.slug_attr, - coordinator, - ) - for instrument in instruments - if instrument.component == "device_tracker" - ) - - async_discover_device([*volvo_data.instruments]) - - config_entry.async_on_unload( - async_dispatcher_connect(hass, VOLVO_DISCOVERY_NEW, async_discover_device) - ) - - -class VolvoTrackerEntity(VolvoEntity, TrackerEntity): - """A tracked Volvo vehicle.""" - - @property - def latitude(self) -> float | None: - """Return latitude value of the device.""" - latitude, _ = self._get_pos() - return latitude - - @property - def longitude(self) -> float | None: - """Return longitude value of the device.""" - _, longitude = self._get_pos() - return longitude - - def _get_pos(self) -> tuple[float, float]: - volvo_data = self.coordinator.volvo_data - instrument = volvo_data.instrument( - self.vin, self.component, self.attribute, self.slug_attr - ) - - latitude, longitude, _, _, _ = instrument.state - - return (float(latitude), float(longitude)) diff --git a/homeassistant/components/volvooncall/entity.py b/homeassistant/components/volvooncall/entity.py deleted file mode 100644 index 5a1194e8b1a..00000000000 --- a/homeassistant/components/volvooncall/entity.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Support for Volvo On Call.""" - -from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.update_coordinator import CoordinatorEntity - -from .const import DOMAIN -from .coordinator import VolvoUpdateCoordinator - - -class VolvoEntity(CoordinatorEntity[VolvoUpdateCoordinator]): - """Base class for all VOC entities.""" - - def __init__( - self, - vin: str, - component: str, - attribute: str, - slug_attr: str, - coordinator: VolvoUpdateCoordinator, - ) -> None: - """Initialize the entity.""" - super().__init__(coordinator) - - self.vin = vin - self.component = component - self.attribute = attribute - self.slug_attr = slug_attr - - @property - def instrument(self): - """Return corresponding instrument.""" - return self.coordinator.volvo_data.instrument( - self.vin, self.component, self.attribute, self.slug_attr - ) - - @property - def icon(self): - """Return the icon.""" - return self.instrument.icon - - @property - def vehicle(self): - """Return vehicle.""" - return self.instrument.vehicle - - @property - def _entity_name(self): - return self.instrument.name - - @property - def _vehicle_name(self): - return self.coordinator.volvo_data.vehicle_name(self.vehicle) - - @property - def name(self): - """Return full name of the entity.""" - return f"{self._vehicle_name} {self._entity_name}" - - @property - def assumed_state(self) -> bool: - """Return true if unable to access real state of entity.""" - return True - - @property - def device_info(self) -> DeviceInfo: - """Return a inique set of attributes for each vehicle.""" - return DeviceInfo( - identifiers={(DOMAIN, self.vehicle.vin)}, - name=self._vehicle_name, - model=self.vehicle.vehicle_type, - manufacturer="Volvo", - ) - - @property - def extra_state_attributes(self): - """Return device specific state attributes.""" - return dict( - self.instrument.attributes, - model=f"{self.vehicle.vehicle_type}/{self.vehicle.model_year}", - ) - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - slug_override = "" - if self.instrument.slug_override is not None: - slug_override = f"-{self.instrument.slug_override}" - return f"{self.vin}-{self.component}-{self.attribute}{slug_override}" diff --git a/homeassistant/components/volvooncall/errors.py b/homeassistant/components/volvooncall/errors.py deleted file mode 100644 index 3736c5b9290..00000000000 --- a/homeassistant/components/volvooncall/errors.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Exceptions specific to volvooncall.""" - -from homeassistant.exceptions import HomeAssistantError - - -class InvalidAuth(HomeAssistantError): - """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/volvooncall/lock.py b/homeassistant/components/volvooncall/lock.py deleted file mode 100644 index 75b54e9dbbc..00000000000 --- a/homeassistant/components/volvooncall/lock.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Support for Volvo On Call locks.""" - -from __future__ import annotations - -from typing import Any - -from volvooncall.dashboard import Instrument, Lock - -from homeassistant.components.lock import LockEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback - -from .const import DOMAIN, VOLVO_DISCOVERY_NEW -from .coordinator import VolvoUpdateCoordinator -from .entity import VolvoEntity - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddConfigEntryEntitiesCallback, -) -> None: - """Configure locks from a config entry created in the integrations UI.""" - coordinator: VolvoUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - volvo_data = coordinator.volvo_data - - @callback - def async_discover_device(instruments: list[Instrument]) -> None: - """Discover and add a discovered Volvo On Call lock.""" - async_add_entities( - VolvoLock( - coordinator, - instrument.vehicle.vin, - instrument.component, - instrument.attr, - instrument.slug_attr, - ) - for instrument in instruments - if instrument.component == "lock" - ) - - async_discover_device([*volvo_data.instruments]) - - config_entry.async_on_unload( - async_dispatcher_connect(hass, VOLVO_DISCOVERY_NEW, async_discover_device) - ) - - -class VolvoLock(VolvoEntity, LockEntity): - """Represents a car lock.""" - - instrument: Lock - - def __init__( - self, - coordinator: VolvoUpdateCoordinator, - vin: str, - component: str, - attribute: str, - slug_attr: str, - ) -> None: - """Initialize the lock.""" - super().__init__(vin, component, attribute, slug_attr, coordinator) - - @property - def is_locked(self) -> bool | None: - """Determine if car is locked.""" - return self.instrument.is_locked - - async def async_lock(self, **kwargs: Any) -> None: - """Lock the car.""" - await self.instrument.lock() - await self.coordinator.async_request_refresh() - - async def async_unlock(self, **kwargs: Any) -> None: - """Unlock the car.""" - await self.instrument.unlock() - await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/volvooncall/manifest.json b/homeassistant/components/volvooncall/manifest.json index 89a35ecde1d..b158cf7ed80 100644 --- a/homeassistant/components/volvooncall/manifest.json +++ b/homeassistant/components/volvooncall/manifest.json @@ -1,10 +1,9 @@ { "domain": "volvooncall", "name": "Volvo On Call", - "codeowners": ["@molobrakos"], + "codeowners": ["@molobrakos", "@svrooij"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/volvooncall", "iot_class": "cloud_polling", - "loggers": ["geopy", "hbmqtt", "volvooncall"], - "requirements": ["volvooncall==0.10.3"] + "quality_scale": "legacy" } diff --git a/homeassistant/components/volvooncall/models.py b/homeassistant/components/volvooncall/models.py deleted file mode 100644 index 159379a908b..00000000000 --- a/homeassistant/components/volvooncall/models.py +++ /dev/null @@ -1,100 +0,0 @@ -"""Support for Volvo On Call.""" - -from aiohttp.client_exceptions import ClientResponseError -from volvooncall import Connection -from volvooncall.dashboard import Instrument - -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_UNIT_SYSTEM -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed -from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.update_coordinator import UpdateFailed - -from .const import ( - CONF_MUTABLE, - PLATFORMS, - UNIT_SYSTEM_IMPERIAL, - UNIT_SYSTEM_SCANDINAVIAN_MILES, - VOLVO_DISCOVERY_NEW, -) -from .errors import InvalidAuth - - -class VolvoData: - """Hold component state.""" - - def __init__( - self, - hass: HomeAssistant, - connection: Connection, - entry: ConfigEntry, - ) -> None: - """Initialize the component state.""" - self.hass = hass - self.vehicles: set[str] = set() - self.instruments: set[Instrument] = set() - self.config_entry = entry - self.connection = connection - - def instrument(self, vin, component, attr, slug_attr): - """Return corresponding instrument.""" - return next( - instrument - for instrument in self.instruments - if instrument.vehicle.vin == vin - and instrument.component == component - and instrument.attr == attr - and instrument.slug_attr == slug_attr - ) - - def vehicle_name(self, vehicle): - """Provide a friendly name for a vehicle.""" - if vehicle.registration_number and vehicle.registration_number != "UNKNOWN": - return vehicle.registration_number - if vehicle.vin: - return vehicle.vin - return "Volvo" - - def discover_vehicle(self, vehicle): - """Load relevant platforms.""" - self.vehicles.add(vehicle.vin) - - dashboard = vehicle.dashboard( - mutable=self.config_entry.data[CONF_MUTABLE], - scandinavian_miles=( - self.config_entry.data[CONF_UNIT_SYSTEM] - == UNIT_SYSTEM_SCANDINAVIAN_MILES - ), - usa_units=( - self.config_entry.data[CONF_UNIT_SYSTEM] == UNIT_SYSTEM_IMPERIAL - ), - ) - - for instrument in ( - instrument - for instrument in dashboard.instruments - if instrument.component in PLATFORMS - ): - self.instruments.add(instrument) - async_dispatcher_send(self.hass, VOLVO_DISCOVERY_NEW, [instrument]) - - async def update(self): - """Update status from the online service.""" - try: - await self.connection.update(journal=True) - except ClientResponseError as ex: - if ex.status == 401: - raise ConfigEntryAuthFailed(ex) from ex - raise UpdateFailed(ex) from ex - - for vehicle in self.connection.vehicles: - if vehicle.vin not in self.vehicles: - self.discover_vehicle(vehicle) - - async def auth_is_valid(self): - """Check if provided username/password/region authenticate.""" - try: - await self.connection.get("customeraccounts") - except ClientResponseError as exc: - raise InvalidAuth from exc diff --git a/homeassistant/components/volvooncall/sensor.py b/homeassistant/components/volvooncall/sensor.py deleted file mode 100644 index feb7248ccaf..00000000000 --- a/homeassistant/components/volvooncall/sensor.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Support for Volvo On Call sensors.""" - -from __future__ import annotations - -from volvooncall.dashboard import Instrument - -from homeassistant.components.sensor import SensorEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback - -from .const import DOMAIN, VOLVO_DISCOVERY_NEW -from .coordinator import VolvoUpdateCoordinator -from .entity import VolvoEntity - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddConfigEntryEntitiesCallback, -) -> None: - """Configure sensors from a config entry created in the integrations UI.""" - coordinator: VolvoUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - volvo_data = coordinator.volvo_data - - @callback - def async_discover_device(instruments: list[Instrument]) -> None: - """Discover and add a discovered Volvo On Call sensor.""" - async_add_entities( - VolvoSensor( - coordinator, - instrument.vehicle.vin, - instrument.component, - instrument.attr, - instrument.slug_attr, - ) - for instrument in instruments - if instrument.component == "sensor" - ) - - async_discover_device([*volvo_data.instruments]) - - config_entry.async_on_unload( - async_dispatcher_connect(hass, VOLVO_DISCOVERY_NEW, async_discover_device) - ) - - -class VolvoSensor(VolvoEntity, SensorEntity): - """Representation of a Volvo sensor.""" - - def __init__( - self, - coordinator: VolvoUpdateCoordinator, - vin: str, - component: str, - attribute: str, - slug_attr: str, - ) -> None: - """Initialize the sensor.""" - super().__init__(vin, component, attribute, slug_attr, coordinator) - self._update_value_and_unit() - - def _update_value_and_unit(self) -> None: - self._attr_native_value = self.instrument.state - self._attr_native_unit_of_measurement = self.instrument.unit - - @callback - def _handle_coordinator_update(self) -> None: - """Handle updated data from the coordinator.""" - self._update_value_and_unit() - self.async_write_ha_state() diff --git a/homeassistant/components/volvooncall/strings.json b/homeassistant/components/volvooncall/strings.json index 8524293d606..72a406273bd 100644 --- a/homeassistant/components/volvooncall/strings.json +++ b/homeassistant/components/volvooncall/strings.json @@ -2,22 +2,17 @@ "config": { "step": { "user": { - "data": { - "username": "[%key:common::config_flow::data::username%]", - "password": "[%key:common::config_flow::data::password%]", - "region": "Region", - "unit_system": "Unit system", - "mutable": "Allow remote start/lock etc." - } + "description": "Volvo on Call is deprecated, use the Volvo integration" } }, - "error": { - "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]" - }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", - "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" + "deprecated": "Volvo On Call has been replaced by the Volvo integration. Please use the Volvo integration instead." + } + }, + "issues": { + "volvooncall_deprecated": { + "title": "Volvo On Call has been replaced", + "description": "The Volvo On Call integration is deprecated and will be removed in 2026.3. Please use the Volvo integration instead.\n\nSteps:\n1. Remove this Volvo On Call integration.\n2. Add the Volvo integration through Settings > Devices & services > Add integration > Volvo.\n3. Follow the setup instructions to authenticate with your Volvo account." } } } diff --git a/homeassistant/components/volvooncall/switch.py b/homeassistant/components/volvooncall/switch.py deleted file mode 100644 index ff321577348..00000000000 --- a/homeassistant/components/volvooncall/switch.py +++ /dev/null @@ -1,78 +0,0 @@ -"""Support for Volvo heater.""" - -from __future__ import annotations - -from typing import Any - -from volvooncall.dashboard import Instrument - -from homeassistant.components.switch import SwitchEntity -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback - -from .const import DOMAIN, VOLVO_DISCOVERY_NEW -from .coordinator import VolvoUpdateCoordinator -from .entity import VolvoEntity - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddConfigEntryEntitiesCallback, -) -> None: - """Configure binary_sensors from a config entry created in the integrations UI.""" - coordinator: VolvoUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] - volvo_data = coordinator.volvo_data - - @callback - def async_discover_device(instruments: list[Instrument]) -> None: - """Discover and add a discovered Volvo On Call switch.""" - async_add_entities( - VolvoSwitch( - coordinator, - instrument.vehicle.vin, - instrument.component, - instrument.attr, - instrument.slug_attr, - ) - for instrument in instruments - if instrument.component == "switch" - ) - - async_discover_device([*volvo_data.instruments]) - - config_entry.async_on_unload( - async_dispatcher_connect(hass, VOLVO_DISCOVERY_NEW, async_discover_device) - ) - - -class VolvoSwitch(VolvoEntity, SwitchEntity): - """Representation of a Volvo switch.""" - - def __init__( - self, - coordinator: VolvoUpdateCoordinator, - vin: str, - component: str, - attribute: str, - slug_attr: str, - ) -> None: - """Initialize the switch.""" - super().__init__(vin, component, attribute, slug_attr, coordinator) - - @property - def is_on(self): - """Determine if switch is on.""" - return self.instrument.state - - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn the switch on.""" - await self.instrument.turn_on() - await self.coordinator.async_request_refresh() - - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn the switch off.""" - await self.instrument.turn_off() - await self.coordinator.async_request_refresh() diff --git a/requirements_all.txt b/requirements_all.txt index 9bc0fc1ab41..56e3db6a2e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3087,9 +3087,6 @@ volkszaehler==0.4.0 # homeassistant.components.volvo volvocarsapi==0.4.2 -# homeassistant.components.volvooncall -volvooncall==0.10.3 - # homeassistant.components.verisure vsure==2.6.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f91022f9366..48e19389e2a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2558,9 +2558,6 @@ voip-utils==0.3.4 # homeassistant.components.volvo volvocarsapi==0.4.2 -# homeassistant.components.volvooncall -volvooncall==0.10.3 - # homeassistant.components.verisure vsure==2.6.7 diff --git a/tests/components/volvooncall/test_config_flow.py b/tests/components/volvooncall/test_config_flow.py index 5268432c17e..206e35dd330 100644 --- a/tests/components/volvooncall/test_config_flow.py +++ b/tests/components/volvooncall/test_config_flow.py @@ -1,9 +1,5 @@ """Test the Volvo On Call config flow.""" -from unittest.mock import Mock, patch - -from aiohttp import ClientResponseError - from homeassistant import config_entries from homeassistant.components.volvooncall.const import DOMAIN from homeassistant.core import HomeAssistant @@ -13,172 +9,27 @@ from tests.common import MockConfigEntry async def test_form(hass: HomeAssistant) -> None: - """Test we get the form.""" + """Test we get an abort with deprecation message.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] is FlowResultType.FORM - assert len(result["errors"]) == 0 - - with ( - patch("volvooncall.Connection.get"), - patch( - "homeassistant.components.volvooncall.async_setup_entry", - return_value=True, - ) as mock_setup_entry, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "username": "test-username", - "password": "test-password", - "region": "na", - "unit_system": "metric", - "mutable": True, - }, - ) - await hass.async_block_till_done() - - assert result2["type"] is FlowResultType.CREATE_ENTRY - assert result2["title"] == "test-username" - assert result2["data"] == { - "username": "test-username", - "password": "test-password", - "region": "na", - "unit_system": "metric", - "mutable": True, - } - assert len(mock_setup_entry.mock_calls) == 1 + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "deprecated" -async def test_form_invalid_auth(hass: HomeAssistant) -> None: - """Test we handle invalid auth.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - exc = ClientResponseError(Mock(), (), status=401) - - with patch( - "volvooncall.Connection.get", - side_effect=exc, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "username": "test-username", - "password": "test-password", - "region": "na", - "unit_system": "metric", - "mutable": True, - }, - ) - - assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": "invalid_auth"} - - -async def test_flow_already_configured(hass: HomeAssistant) -> None: - """Test we handle a flow that has already been configured.""" - first_entry = MockConfigEntry(domain=DOMAIN, unique_id="test-username") - first_entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] is FlowResultType.FORM - assert len(result["errors"]) == 0 - - with ( - patch("volvooncall.Connection.get"), - patch( - "homeassistant.components.volvooncall.async_setup_entry", - return_value=True, - ), - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "username": "test-username", - "password": "test-password", - "region": "na", - "unit_system": "metric", - "mutable": True, - }, - ) - await hass.async_block_till_done() - - assert result2["type"] is FlowResultType.ABORT - assert result2["reason"] == "already_configured" - - -async def test_form_other_exception(hass: HomeAssistant) -> None: - """Test we handle other exceptions.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "volvooncall.Connection.get", - side_effect=Exception, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "username": "test-username", - "password": "test-password", - "region": "na", - "unit_system": "metric", - "mutable": True, - }, - ) - - assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": "unknown"} - - -async def test_reauth(hass: HomeAssistant) -> None: - """Test that we handle the reauth flow.""" - - first_entry = MockConfigEntry( +async def test_flow_aborts_with_existing_config_entry(hass: HomeAssistant) -> None: + """Test the config flow aborts even with existing config entries.""" + # Create an existing config entry + entry = MockConfigEntry( domain=DOMAIN, - unique_id="test-username", - data={ - "username": "test-username", - "password": "test-password", - "region": "na", - "unit_system": "metric", - "mutable": True, - }, + title="Volvo On Call", + data={}, ) - first_entry.add_to_hass(hass) + entry.add_to_hass(hass) - result = await first_entry.start_reauth_flow(hass) - - # the first form is just the confirmation prompt - assert result["type"] is FlowResultType.FORM - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {}, + # New flow should still abort + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} ) - await hass.async_block_till_done() - - # the second form is the user flow where reauth happens - assert result2["type"] is FlowResultType.FORM - - with patch("volvooncall.Connection.get"): - result3 = await hass.config_entries.flow.async_configure( - result2["flow_id"], - { - "username": "test-username", - "password": "test-new-password", - "region": "na", - "unit_system": "metric", - "mutable": True, - }, - ) - await hass.async_block_till_done() - - assert result3["type"] is FlowResultType.ABORT - assert result3["reason"] == "reauth_successful" + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "deprecated" diff --git a/tests/components/volvooncall/test_init.py b/tests/components/volvooncall/test_init.py new file mode 100644 index 00000000000..a0b65fad659 --- /dev/null +++ b/tests/components/volvooncall/test_init.py @@ -0,0 +1,76 @@ +"""Test the Volvo On Call integration setup.""" + +from homeassistant.components.volvooncall.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry as ir + +from tests.common import MockConfigEntry + + +async def test_setup_entry_creates_repair_issue( + hass: HomeAssistant, issue_registry: ir.IssueRegistry +) -> None: + """Test that setup creates a repair issue.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Volvo On Call", + data={}, + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.LOADED + issue = issue_registry.async_get_issue(DOMAIN, "volvooncall_deprecated") + + assert issue is not None + assert issue.severity is ir.IssueSeverity.WARNING + assert issue.translation_key == "volvooncall_deprecated" + + +async def test_unload_entry_removes_repair_issue( + hass: HomeAssistant, issue_registry: ir.IssueRegistry +) -> None: + """Test that unloading the last config entry removes the repair issue.""" + first_config_entry = MockConfigEntry( + domain=DOMAIN, + title="Volvo On Call", + data={}, + ) + first_config_entry.add_to_hass(hass) + second_config_entry = MockConfigEntry( + domain=DOMAIN, + title="Volvo On Call second", + data={}, + ) + second_config_entry.add_to_hass(hass) + + # Setup entry + assert await hass.config_entries.async_setup(first_config_entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 2 + + # Check that the repair issue was created + issue = issue_registry.async_get_issue(DOMAIN, "volvooncall_deprecated") + assert issue is not None + + # Unload entry (this is the only entry, so issue should be removed) + assert await hass.config_entries.async_remove(first_config_entry.entry_id) + await hass.async_block_till_done(wait_background_tasks=True) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + # Check that the repair issue still exists because there's another entry + issue = issue_registry.async_get_issue(DOMAIN, "volvooncall_deprecated") + assert issue is not None + + # Unload entry (this is the only entry, so issue should be removed) + assert await hass.config_entries.async_remove(second_config_entry.entry_id) + await hass.async_block_till_done(wait_background_tasks=True) + + # Check that the repair issue was removed + issue = issue_registry.async_get_issue(DOMAIN, "volvooncall_deprecated") + assert issue is None