From bcf97cb3081b5dfd70e1bdd3a4a013c74b1ee8b2 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 1 Sep 2021 13:10:48 +0200 Subject: [PATCH] Add device tracker platform to Renault integration (#54745) --- homeassistant/components/renault/const.py | 2 + .../components/renault/device_tracker.py | 61 +++++++ .../components/renault/renault_entities.py | 20 ++- .../components/renault/renault_vehicle.py | 5 + tests/components/renault/__init__.py | 14 ++ tests/components/renault/const.py | 101 +++++++---- .../components/renault/test_device_tracker.py | 164 ++++++++++++++++++ tests/fixtures/renault/location.json | 11 ++ 8 files changed, 343 insertions(+), 35 deletions(-) create mode 100644 homeassistant/components/renault/device_tracker.py create mode 100644 tests/components/renault/test_device_tracker.py create mode 100644 tests/fixtures/renault/location.json diff --git a/homeassistant/components/renault/const.py b/homeassistant/components/renault/const.py index 824779a4d3e..e080e2b5962 100644 --- a/homeassistant/components/renault/const.py +++ b/homeassistant/components/renault/const.py @@ -1,5 +1,6 @@ """Constants for the Renault component.""" from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN DOMAIN = "renault" @@ -11,6 +12,7 @@ DEFAULT_SCAN_INTERVAL = 300 # 5 minutes PLATFORMS = [ BINARY_SENSOR_DOMAIN, + DEVICE_TRACKER_DOMAIN, SENSOR_DOMAIN, ] diff --git a/homeassistant/components/renault/device_tracker.py b/homeassistant/components/renault/device_tracker.py new file mode 100644 index 00000000000..466a1f9e4a6 --- /dev/null +++ b/homeassistant/components/renault/device_tracker.py @@ -0,0 +1,61 @@ +"""Support for Renault device trackers.""" +from __future__ import annotations + +from renault_api.kamereon.models import KamereonVehicleLocationData + +from homeassistant.components.device_tracker import SOURCE_TYPE_GPS +from homeassistant.components.device_tracker.config_entry import TrackerEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .renault_entities import RenaultDataEntity, RenaultEntityDescription +from .renault_hub import RenaultHub + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Renault entities from config entry.""" + proxy: RenaultHub = hass.data[DOMAIN][config_entry.entry_id] + entities: list[RenaultDeviceTracker] = [ + RenaultDeviceTracker(vehicle, description) + for vehicle in proxy.vehicles.values() + for description in DEVICE_TRACKER_TYPES + if description.coordinator in vehicle.coordinators + ] + async_add_entities(entities) + + +class RenaultDeviceTracker( + RenaultDataEntity[KamereonVehicleLocationData], TrackerEntity +): + """Mixin for device tracker specific attributes.""" + + @property + def latitude(self) -> float | None: + """Return latitude value of the device.""" + return self.coordinator.data.gpsLatitude if self.coordinator.data else None + + @property + def longitude(self) -> float | None: + """Return longitude value of the device.""" + return self.coordinator.data.gpsLongitude if self.coordinator.data else None + + @property + def source_type(self) -> str: + """Return the source type of the device.""" + return SOURCE_TYPE_GPS + + +DEVICE_TRACKER_TYPES: tuple[RenaultEntityDescription, ...] = ( + RenaultEntityDescription( + key="location", + coordinator="location", + icon="mdi:car", + name="Location", + ), +) diff --git a/homeassistant/components/renault/renault_entities.py b/homeassistant/components/renault/renault_entities.py index 29d1aa4b860..e0aae72298b 100644 --- a/homeassistant/components/renault/renault_entities.py +++ b/homeassistant/components/renault/renault_entities.py @@ -3,11 +3,12 @@ from __future__ import annotations from collections.abc import Mapping from dataclasses import dataclass -from typing import Any, Optional, cast +from typing import TYPE_CHECKING, Any, Optional, cast from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.typing import StateType from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.util.dt import as_utc, parse_datetime from .renault_coordinator import T from .renault_vehicle import RenaultVehicleProxy @@ -54,8 +55,19 @@ class RenaultDataEntity(CoordinatorEntity[Optional[T]], Entity): @property def extra_state_attributes(self) -> Mapping[str, Any] | None: """Return the state attributes of this entity.""" + last_update: str | None = None if self.entity_description.coordinator == "battery": - last_update = self._get_data_attr("timestamp") - if last_update: - return {ATTR_LAST_UPDATE: last_update} + last_update = cast(str, self._get_data_attr("timestamp")) + elif self.entity_description.coordinator == "location": + last_update = cast(str, self._get_data_attr("lastUpdateTime")) + if last_update: + return {ATTR_LAST_UPDATE: _convert_to_utc_string(last_update)} return None + + +def _convert_to_utc_string(value: str) -> str: + """Convert date to UTC iso format.""" + original_dt = parse_datetime(value) + if TYPE_CHECKING: + assert original_dt is not None + return as_utc(original_dt).isoformat() diff --git a/homeassistant/components/renault/renault_vehicle.py b/homeassistant/components/renault/renault_vehicle.py index 1c21b21843d..90bc4a2def4 100644 --- a/homeassistant/components/renault/renault_vehicle.py +++ b/homeassistant/components/renault/renault_vehicle.py @@ -138,6 +138,11 @@ COORDINATORS: tuple[RenaultCoordinatorDescription, ...] = ( key="hvac_status", update_method=lambda x: x.get_hvac_status, ), + RenaultCoordinatorDescription( + endpoint="location", + key="location", + update_method=lambda x: x.get_location, + ), RenaultCoordinatorDescription( endpoint="battery-status", key="battery", diff --git a/tests/components/renault/__init__.py b/tests/components/renault/__init__.py index f77c4bcd40a..bbca3a74139 100644 --- a/tests/components/renault/__init__.py +++ b/tests/components/renault/__init__.py @@ -63,6 +63,11 @@ def get_fixtures(vehicle_type: str) -> dict[str, Any]: if "hvac_status" in mock_vehicle["endpoints"] else load_fixture("renault/no_data.json") ).get_attributes(schemas.KamereonVehicleHvacStatusDataSchema), + "location": schemas.KamereonVehicleDataResponseSchema.loads( + load_fixture(f"renault/{mock_vehicle['endpoints']['location']}") + if "location" in mock_vehicle["endpoints"] + else load_fixture("renault/no_data.json") + ).get_attributes(schemas.KamereonVehicleLocationDataSchema), } @@ -132,6 +137,9 @@ async def setup_renault_integration_vehicle(hass: HomeAssistant, vehicle_type: s ), patch( "renault_api.renault_vehicle.RenaultVehicle.get_hvac_status", return_value=mock_fixtures["hvac_status"], + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.get_location", + return_value=mock_fixtures["location"], ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -181,6 +189,9 @@ async def setup_renault_integration_vehicle_with_no_data( ), patch( "renault_api.renault_vehicle.RenaultVehicle.get_hvac_status", return_value=mock_fixtures["hvac_status"], + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.get_location", + return_value=mock_fixtures["location"], ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -229,6 +240,9 @@ async def setup_renault_integration_vehicle_with_side_effect( ), patch( "renault_api.renault_vehicle.RenaultVehicle.get_hvac_status", side_effect=side_effect, + ), patch( + "renault_api.renault_vehicle.RenaultVehicle.get_location", + side_effect=side_effect, ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/renault/const.py b/tests/components/renault/const.py index e955f24018a..cbc94c61bf4 100644 --- a/tests/components/renault/const.py +++ b/tests/components/renault/const.py @@ -4,6 +4,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_PLUG, DOMAIN as BINARY_SENSOR_DOMAIN, ) +from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN from homeassistant.components.renault.const import ( CONF_KAMEREON_ACCOUNT_ID, CONF_LOCALE, @@ -33,6 +34,7 @@ from homeassistant.const import ( LENGTH_KILOMETERS, PERCENTAGE, POWER_KILO_WATT, + STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_UNKNOWN, @@ -77,6 +79,7 @@ MOCK_VEHICLES = { "endpoints_available": [ True, # cockpit True, # hvac-status + False, # location True, # battery-status True, # charge-mode ], @@ -92,23 +95,24 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777999_plugged_in", "result": STATE_ON, ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG, - ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", + ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", }, { "entity_id": "binary_sensor.charging", "unique_id": "vf1aaaaa555777999_charging", "result": STATE_ON, ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY_CHARGING, - ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", + ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", }, ], + DEVICE_TRACKER_DOMAIN: [], SENSOR_DOMAIN: [ { "entity_id": "sensor.battery_autonomy", "unique_id": "vf1aaaaa555777999_battery_autonomy", "result": "141", ATTR_ICON: "mdi:ev-station", - ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", + ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, @@ -117,7 +121,7 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777999_battery_available_energy", "result": "31", ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", + ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, }, @@ -126,7 +130,7 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777999_battery_level", "result": "60", ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY, - ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", + ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, @@ -135,7 +139,7 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777999_battery_temperature", "result": "20", ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", + ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, @@ -152,14 +156,14 @@ MOCK_VEHICLES = { "result": "charge_in_progress", ATTR_DEVICE_CLASS: DEVICE_CLASS_CHARGE_STATE, ATTR_ICON: "mdi:flash", - ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", + ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", }, { "entity_id": "sensor.charging_power", "unique_id": "vf1aaaaa555777999_charging_power", "result": "0.027", ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, - ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", + ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT, }, @@ -168,7 +172,7 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777999_charging_remaining_time", "result": "145", ATTR_ICON: "mdi:timer", - ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", + ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: TIME_MINUTES, }, @@ -194,7 +198,7 @@ MOCK_VEHICLES = { "result": "plugged", ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG_STATE, ATTR_ICON: "mdi:power-plug", - ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", + ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", }, ], }, @@ -209,6 +213,7 @@ MOCK_VEHICLES = { "endpoints_available": [ True, # cockpit False, # hvac-status + True, # location True, # battery-status True, # charge-mode ], @@ -216,6 +221,7 @@ MOCK_VEHICLES = { "battery_status": "battery_status_not_charging.json", "charge_mode": "charge_mode_schedule.json", "cockpit": "cockpit_ev.json", + "location": "location.json", }, BINARY_SENSOR_DOMAIN: [ { @@ -223,23 +229,32 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777999_plugged_in", "result": STATE_OFF, ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG, - ATTR_LAST_UPDATE: "2020-11-17T09:06:48+01:00", + ATTR_LAST_UPDATE: "2020-11-17T08:06:48+00:00", }, { "entity_id": "binary_sensor.charging", "unique_id": "vf1aaaaa555777999_charging", "result": STATE_OFF, ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY_CHARGING, - ATTR_LAST_UPDATE: "2020-11-17T09:06:48+01:00", + ATTR_LAST_UPDATE: "2020-11-17T08:06:48+00:00", }, ], + DEVICE_TRACKER_DOMAIN: [ + { + "entity_id": "device_tracker.location", + "unique_id": "vf1aaaaa555777999_location", + "result": STATE_NOT_HOME, + ATTR_ICON: "mdi:car", + ATTR_LAST_UPDATE: "2020-02-18T16:58:38+00:00", + } + ], SENSOR_DOMAIN: [ { "entity_id": "sensor.battery_autonomy", "unique_id": "vf1aaaaa555777999_battery_autonomy", "result": "128", ATTR_ICON: "mdi:ev-station", - ATTR_LAST_UPDATE: "2020-11-17T09:06:48+01:00", + ATTR_LAST_UPDATE: "2020-11-17T08:06:48+00:00", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, @@ -248,7 +263,7 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777999_battery_available_energy", "result": "0", ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_LAST_UPDATE: "2020-11-17T09:06:48+01:00", + ATTR_LAST_UPDATE: "2020-11-17T08:06:48+00:00", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, }, @@ -257,7 +272,7 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777999_battery_level", "result": "50", ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY, - ATTR_LAST_UPDATE: "2020-11-17T09:06:48+01:00", + ATTR_LAST_UPDATE: "2020-11-17T08:06:48+00:00", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, @@ -266,7 +281,7 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777999_battery_temperature", "result": STATE_UNKNOWN, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_LAST_UPDATE: "2020-11-17T09:06:48+01:00", + ATTR_LAST_UPDATE: "2020-11-17T08:06:48+00:00", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, @@ -283,14 +298,14 @@ MOCK_VEHICLES = { "result": "charge_error", ATTR_DEVICE_CLASS: DEVICE_CLASS_CHARGE_STATE, ATTR_ICON: "mdi:flash-off", - ATTR_LAST_UPDATE: "2020-11-17T09:06:48+01:00", + ATTR_LAST_UPDATE: "2020-11-17T08:06:48+00:00", }, { "entity_id": "sensor.charging_power", "unique_id": "vf1aaaaa555777999_charging_power", "result": STATE_UNKNOWN, ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, - ATTR_LAST_UPDATE: "2020-11-17T09:06:48+01:00", + ATTR_LAST_UPDATE: "2020-11-17T08:06:48+00:00", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT, }, @@ -299,7 +314,7 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777999_charging_remaining_time", "result": STATE_UNKNOWN, ATTR_ICON: "mdi:timer", - ATTR_LAST_UPDATE: "2020-11-17T09:06:48+01:00", + ATTR_LAST_UPDATE: "2020-11-17T08:06:48+00:00", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: TIME_MINUTES, }, @@ -317,7 +332,7 @@ MOCK_VEHICLES = { "result": "unplugged", ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG_STATE, ATTR_ICON: "mdi:power-plug-off", - ATTR_LAST_UPDATE: "2020-11-17T09:06:48+01:00", + ATTR_LAST_UPDATE: "2020-11-17T08:06:48+00:00", }, ], }, @@ -332,6 +347,7 @@ MOCK_VEHICLES = { "endpoints_available": [ True, # cockpit False, # hvac-status + True, # location True, # battery-status True, # charge-mode ], @@ -339,6 +355,7 @@ MOCK_VEHICLES = { "battery_status": "battery_status_charging.json", "charge_mode": "charge_mode_always.json", "cockpit": "cockpit_fuel.json", + "location": "location.json", }, BINARY_SENSOR_DOMAIN: [ { @@ -346,23 +363,32 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777123_plugged_in", "result": STATE_ON, ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG, - ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", + ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", }, { "entity_id": "binary_sensor.charging", "unique_id": "vf1aaaaa555777123_charging", "result": STATE_ON, ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY_CHARGING, - ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", + ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", }, ], + DEVICE_TRACKER_DOMAIN: [ + { + "entity_id": "device_tracker.location", + "unique_id": "vf1aaaaa555777123_location", + "result": STATE_NOT_HOME, + ATTR_ICON: "mdi:car", + ATTR_LAST_UPDATE: "2020-02-18T16:58:38+00:00", + } + ], SENSOR_DOMAIN: [ { "entity_id": "sensor.battery_autonomy", "unique_id": "vf1aaaaa555777123_battery_autonomy", "result": "141", ATTR_ICON: "mdi:ev-station", - ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", + ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, }, @@ -371,7 +397,7 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777123_battery_available_energy", "result": "31", ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, - ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", + ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, }, @@ -380,7 +406,7 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777123_battery_level", "result": "60", ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY, - ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", + ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, }, @@ -389,7 +415,7 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777123_battery_temperature", "result": "20", ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", + ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, }, @@ -406,14 +432,14 @@ MOCK_VEHICLES = { "result": "charge_in_progress", ATTR_DEVICE_CLASS: DEVICE_CLASS_CHARGE_STATE, ATTR_ICON: "mdi:flash", - ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", + ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", }, { "entity_id": "sensor.charging_power", "unique_id": "vf1aaaaa555777123_charging_power", "result": "27.0", ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, - ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", + ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: POWER_KILO_WATT, }, @@ -422,7 +448,7 @@ MOCK_VEHICLES = { "unique_id": "vf1aaaaa555777123_charging_remaining_time", "result": "145", ATTR_ICON: "mdi:timer", - ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", + ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, ATTR_UNIT_OF_MEASUREMENT: TIME_MINUTES, }, @@ -456,7 +482,7 @@ MOCK_VEHICLES = { "result": "plugged", ATTR_DEVICE_CLASS: DEVICE_CLASS_PLUG_STATE, ATTR_ICON: "mdi:power-plug", - ATTR_LAST_UPDATE: "2020-01-12T21:40:16Z", + ATTR_LAST_UPDATE: "2020-01-12T21:40:16+00:00", }, ], }, @@ -471,11 +497,24 @@ MOCK_VEHICLES = { "endpoints_available": [ True, # cockpit False, # hvac-status + True, # location # Ignore, # battery-status # Ignore, # charge-mode ], - "endpoints": {"cockpit": "cockpit_fuel.json"}, + "endpoints": { + "cockpit": "cockpit_fuel.json", + "location": "location.json", + }, BINARY_SENSOR_DOMAIN: [], + DEVICE_TRACKER_DOMAIN: [ + { + "entity_id": "device_tracker.location", + "unique_id": "vf1aaaaa555777123_location", + "result": STATE_NOT_HOME, + ATTR_ICON: "mdi:car", + ATTR_LAST_UPDATE: "2020-02-18T16:58:38+00:00", + } + ], SENSOR_DOMAIN: [ { "entity_id": "sensor.fuel_autonomy", diff --git a/tests/components/renault/test_device_tracker.py b/tests/components/renault/test_device_tracker.py new file mode 100644 index 00000000000..f6cac06380b --- /dev/null +++ b/tests/components/renault/test_device_tracker.py @@ -0,0 +1,164 @@ +"""Tests for Renault sensors.""" +from unittest.mock import patch + +import pytest +from renault_api.kamereon import exceptions + +from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN +from homeassistant.components.renault.renault_entities import ATTR_LAST_UPDATE +from homeassistant.const import ATTR_ICON, STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from . import ( + check_device_registry, + get_no_data_icon, + setup_renault_integration_vehicle, + setup_renault_integration_vehicle_with_no_data, + setup_renault_integration_vehicle_with_side_effect, +) +from .const import DYNAMIC_ATTRIBUTES, FIXED_ATTRIBUTES, MOCK_VEHICLES + +from tests.common import mock_device_registry, mock_registry + + +@pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) +async def test_device_trackers(hass: HomeAssistant, vehicle_type: str): + """Test for Renault device trackers.""" + await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + with patch("homeassistant.components.renault.PLATFORMS", [DEVICE_TRACKER_DOMAIN]): + await setup_renault_integration_vehicle(hass, vehicle_type) + await hass.async_block_till_done() + + mock_vehicle = MOCK_VEHICLES[vehicle_type] + check_device_registry(device_registry, mock_vehicle["expected_device"]) + + expected_entities = mock_vehicle[DEVICE_TRACKER_DOMAIN] + assert len(entity_registry.entities) == len(expected_entities) + for expected_entity in expected_entities: + entity_id = expected_entity["entity_id"] + registry_entry = entity_registry.entities.get(entity_id) + assert registry_entry is not None + assert registry_entry.unique_id == expected_entity["unique_id"] + state = hass.states.get(entity_id) + assert state.state == expected_entity["result"] + for attr in FIXED_ATTRIBUTES + DYNAMIC_ATTRIBUTES: + assert state.attributes.get(attr) == expected_entity.get(attr) + + +@pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) +async def test_device_tracker_empty(hass: HomeAssistant, vehicle_type: str): + """Test for Renault device trackers with empty data from Renault.""" + await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + with patch("homeassistant.components.renault.PLATFORMS", [DEVICE_TRACKER_DOMAIN]): + await setup_renault_integration_vehicle_with_no_data(hass, vehicle_type) + await hass.async_block_till_done() + + mock_vehicle = MOCK_VEHICLES[vehicle_type] + check_device_registry(device_registry, mock_vehicle["expected_device"]) + + expected_entities = mock_vehicle[DEVICE_TRACKER_DOMAIN] + assert len(entity_registry.entities) == len(expected_entities) + for expected_entity in expected_entities: + entity_id = expected_entity["entity_id"] + registry_entry = entity_registry.entities.get(entity_id) + assert registry_entry is not None + assert registry_entry.unique_id == expected_entity["unique_id"] + state = hass.states.get(entity_id) + assert state.state == STATE_UNKNOWN + for attr in FIXED_ATTRIBUTES: + assert state.attributes.get(attr) == expected_entity.get(attr) + # Check dynamic attributes: + assert state.attributes.get(ATTR_ICON) == get_no_data_icon(expected_entity) + assert ATTR_LAST_UPDATE not in state.attributes + + +@pytest.mark.parametrize("vehicle_type", MOCK_VEHICLES.keys()) +async def test_device_tracker_errors(hass: HomeAssistant, vehicle_type: str): + """Test for Renault device trackers with temporary failure.""" + await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + invalid_upstream_exception = exceptions.InvalidUpstreamException( + "err.tech.500", + "Invalid response from the upstream server (The request sent to the GDC is erroneous) ; 502 Bad Gateway", + ) + + with patch("homeassistant.components.renault.PLATFORMS", [DEVICE_TRACKER_DOMAIN]): + await setup_renault_integration_vehicle_with_side_effect( + hass, vehicle_type, invalid_upstream_exception + ) + await hass.async_block_till_done() + + mock_vehicle = MOCK_VEHICLES[vehicle_type] + check_device_registry(device_registry, mock_vehicle["expected_device"]) + + expected_entities = mock_vehicle[DEVICE_TRACKER_DOMAIN] + assert len(entity_registry.entities) == len(expected_entities) + for expected_entity in expected_entities: + entity_id = expected_entity["entity_id"] + registry_entry = entity_registry.entities.get(entity_id) + assert registry_entry is not None + assert registry_entry.unique_id == expected_entity["unique_id"] + state = hass.states.get(entity_id) + assert state.state == STATE_UNAVAILABLE + for attr in FIXED_ATTRIBUTES: + assert state.attributes.get(attr) == expected_entity.get(attr) + # Check dynamic attributes: + assert state.attributes.get(ATTR_ICON) == get_no_data_icon(expected_entity) + assert ATTR_LAST_UPDATE not in state.attributes + + +async def test_device_tracker_access_denied(hass: HomeAssistant): + """Test for Renault device trackers with access denied failure.""" + await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + vehicle_type = "zoe_40" + access_denied_exception = exceptions.AccessDeniedException( + "err.func.403", + "Access is denied for this resource", + ) + + with patch("homeassistant.components.renault.PLATFORMS", [DEVICE_TRACKER_DOMAIN]): + await setup_renault_integration_vehicle_with_side_effect( + hass, vehicle_type, access_denied_exception + ) + await hass.async_block_till_done() + + mock_vehicle = MOCK_VEHICLES[vehicle_type] + check_device_registry(device_registry, mock_vehicle["expected_device"]) + + assert len(entity_registry.entities) == 0 + + +async def test_device_tracker_not_supported(hass: HomeAssistant): + """Test for Renault device trackers with not supported failure.""" + await async_setup_component(hass, "persistent_notification", {}) + entity_registry = mock_registry(hass) + device_registry = mock_device_registry(hass) + + vehicle_type = "zoe_40" + not_supported_exception = exceptions.NotSupportedException( + "err.tech.501", + "This feature is not technically supported by this gateway", + ) + + with patch("homeassistant.components.renault.PLATFORMS", [DEVICE_TRACKER_DOMAIN]): + await setup_renault_integration_vehicle_with_side_effect( + hass, vehicle_type, not_supported_exception + ) + await hass.async_block_till_done() + + mock_vehicle = MOCK_VEHICLES[vehicle_type] + check_device_registry(device_registry, mock_vehicle["expected_device"]) + + assert len(entity_registry.entities) == 0 diff --git a/tests/fixtures/renault/location.json b/tests/fixtures/renault/location.json new file mode 100644 index 00000000000..bae4474521f --- /dev/null +++ b/tests/fixtures/renault/location.json @@ -0,0 +1,11 @@ +{ + "data": { + "type": "Car", + "id": "VF1AAAAA555777999", + "attributes": { + "gpsLatitude": 48.1234567, + "gpsLongitude": 11.1234567, + "lastUpdateTime": "2020-02-18T16:58:38Z" + } + } +}