diff --git a/.strict-typing b/.strict-typing index f574aeb79d6..ac5d2b6a8ac 100644 --- a/.strict-typing +++ b/.strict-typing @@ -96,6 +96,7 @@ homeassistant.components.persistent_notification.* homeassistant.components.pi_hole.* homeassistant.components.proximity.* homeassistant.components.rainmachine.* +homeassistant.components.rdw.* homeassistant.components.recollect_waste.* homeassistant.components.recorder.purge homeassistant.components.recorder.repack diff --git a/CODEOWNERS b/CODEOWNERS index 21e6526b03f..84ab3a80c5e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -425,6 +425,7 @@ homeassistant/components/raincloud/* @vanstinator homeassistant/components/rainforest_eagle/* @gtdiehl @jcalbert homeassistant/components/rainmachine/* @bachya homeassistant/components/random/* @fabaff +homeassistant/components/rdw/* @frenck homeassistant/components/recollect_waste/* @bachya homeassistant/components/recorder/* @home-assistant/core homeassistant/components/rejseplanen/* @DarkFox diff --git a/homeassistant/components/rdw/__init__.py b/homeassistant/components/rdw/__init__.py new file mode 100644 index 00000000000..32f5c81e86a --- /dev/null +++ b/homeassistant/components/rdw/__init__.py @@ -0,0 +1,42 @@ +"""Support for RDW.""" +from __future__ import annotations + +from vehicle import RDW, Vehicle + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import CONF_LICENSE_PLATE, DOMAIN, LOGGER, SCAN_INTERVAL + +PLATFORMS = (SENSOR_DOMAIN,) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up RDW from a config entry.""" + session = async_get_clientsession(hass) + rdw = RDW(session=session, license_plate=entry.data[CONF_LICENSE_PLATE]) + + coordinator: DataUpdateCoordinator[Vehicle] = DataUpdateCoordinator( + hass, + LOGGER, + name=f"{DOMAIN}_APK", + update_interval=SCAN_INTERVAL, + update_method=rdw.vehicle, + ) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload RDW config entry.""" + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + del hass.data[DOMAIN][entry.entry_id] + return unload_ok diff --git a/homeassistant/components/rdw/config_flow.py b/homeassistant/components/rdw/config_flow.py new file mode 100644 index 00000000000..a9fedc88dac --- /dev/null +++ b/homeassistant/components/rdw/config_flow.py @@ -0,0 +1,56 @@ +"""Config flow to configure the RDW integration.""" +from __future__ import annotations + +from typing import Any + +from vehicle import RDW, RDWError, RDWUnknownLicensePlateError +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import CONF_LICENSE_PLATE, DOMAIN + + +class RDWFlowHandler(ConfigFlow, domain=DOMAIN): + """Config flow for RDW.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + errors = {} + + if user_input is not None: + session = async_get_clientsession(self.hass) + rdw = RDW(session=session) + try: + vehicle = await rdw.vehicle( + license_plate=user_input[CONF_LICENSE_PLATE] + ) + except RDWUnknownLicensePlateError: + errors["base"] = "unknown_license_plate" + except RDWError: + errors["base"] = "cannot_connect" + else: + await self.async_set_unique_id(vehicle.license_plate) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=user_input[CONF_LICENSE_PLATE], + data={ + CONF_LICENSE_PLATE: vehicle.license_plate, + }, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_LICENSE_PLATE): str, + } + ), + errors=errors, + ) diff --git a/homeassistant/components/rdw/const.py b/homeassistant/components/rdw/const.py new file mode 100644 index 00000000000..10058019aa2 --- /dev/null +++ b/homeassistant/components/rdw/const.py @@ -0,0 +1,14 @@ +"""Constants for the RDW integration.""" +from __future__ import annotations + +from datetime import timedelta +import logging +from typing import Final + +DOMAIN: Final = "rdw" + +LOGGER = logging.getLogger(__package__) +SCAN_INTERVAL = timedelta(hours=1) + +ENTRY_TYPE_SERVICE: Final = "service" +CONF_LICENSE_PLATE: Final = "license_plate" diff --git a/homeassistant/components/rdw/manifest.json b/homeassistant/components/rdw/manifest.json new file mode 100644 index 00000000000..d7614c5bfa9 --- /dev/null +++ b/homeassistant/components/rdw/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "rdw", + "name": "RDW", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/rdw", + "requirements": ["vehicle==0.1.0"], + "codeowners": ["@frenck"], + "quality_scale": "platinum", + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/rdw/sensor.py b/homeassistant/components/rdw/sensor.py new file mode 100644 index 00000000000..c6fc7157494 --- /dev/null +++ b/homeassistant/components/rdw/sensor.py @@ -0,0 +1,103 @@ +"""Support for RDW sensors.""" +from __future__ import annotations + +from dataclasses import dataclass +from typing import Callable + +from vehicle import Vehicle + +from homeassistant.components.sensor import ( + DEVICE_CLASS_DATE, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import StateType +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import CONF_LICENSE_PLATE, DOMAIN, ENTRY_TYPE_SERVICE + + +@dataclass +class RDWSensorEntityDescriptionMixin: + """Mixin for required keys.""" + + value_fn: Callable[[Vehicle], str | float | None] + + +@dataclass +class RDWSensorEntityDescription( + SensorEntityDescription, RDWSensorEntityDescriptionMixin +): + """Describes RDW sensor entity.""" + + +SENSORS: tuple[RDWSensorEntityDescription, ...] = ( + RDWSensorEntityDescription( + key="apk_expiration", + name="APK Expiration", + device_class=DEVICE_CLASS_DATE, + value_fn=lambda vehicle: vehicle.apk_expiration.isoformat(), + ), + RDWSensorEntityDescription( + key="name_registration_date", + name="Name Registration Date", + device_class=DEVICE_CLASS_DATE, + value_fn=lambda vehicle: vehicle.name_registration_date.isoformat(), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up RDW sensors based on a config entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + RDWSensorEntity( + coordinator=coordinator, + license_plate=entry.data[CONF_LICENSE_PLATE], + description=description, + ) + for description in SENSORS + ) + + +class RDWSensorEntity(CoordinatorEntity, SensorEntity): + """Defines an RDW sensor.""" + + entity_description: RDWSensorEntityDescription + + def __init__( + self, + *, + coordinator: DataUpdateCoordinator, + license_plate: str, + description: RDWSensorEntityDescription, + ) -> None: + """Initialize RDW sensor.""" + super().__init__(coordinator=coordinator) + self.entity_description = description + self._attr_unique_id = f"{license_plate}_{description.key}" + + self._attr_device_info = DeviceInfo( + entry_type=ENTRY_TYPE_SERVICE, + identifiers={(DOMAIN, f"{license_plate}")}, + manufacturer=coordinator.data.brand, + name=f"{coordinator.data.brand}: {coordinator.data.license_plate}", + model=coordinator.data.model, + configuration_url=f"https://ovi.rdw.nl/default.aspx?kenteken={coordinator.data.license_plate}", + ) + + @property + def native_value(self) -> StateType: + """Return the state of the sensor.""" + return self.entity_description.value_fn(self.coordinator.data) diff --git a/homeassistant/components/rdw/strings.json b/homeassistant/components/rdw/strings.json new file mode 100644 index 00000000000..48bcd8c0c5d --- /dev/null +++ b/homeassistant/components/rdw/strings.json @@ -0,0 +1,15 @@ +{ + "config": { + "step": { + "user": { + "data": { + "license_plate": "License plate" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown_license_plate": "Unknown license plate" + } + } +} diff --git a/homeassistant/components/rdw/translations/en.json b/homeassistant/components/rdw/translations/en.json new file mode 100644 index 00000000000..9d2827ed4de --- /dev/null +++ b/homeassistant/components/rdw/translations/en.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Failed to connect", + "unknown_license_plate": "Unknown license plate" + }, + "step": { + "user": { + "data": { + "license_plate": "License plate" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index bff1305504f..f65cf964ef3 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -235,6 +235,7 @@ FLOWS = [ "rachio", "rainforest_eagle", "rainmachine", + "rdw", "recollect_waste", "renault", "rfxtrx", diff --git a/mypy.ini b/mypy.ini index 425cfd1aa57..ccacbca2da2 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1067,6 +1067,17 @@ no_implicit_optional = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.rdw.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.recollect_waste.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index cb99f907ace..92f9826c0ac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2365,6 +2365,9 @@ uvcclient==0.11.0 # homeassistant.components.vallox vallox-websocket-api==2.8.1 +# homeassistant.components.rdw +vehicle==0.1.0 + # homeassistant.components.velbus velbus-aio==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a32bc0d5ea8..537f8c31be4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1372,6 +1372,9 @@ url-normalize==1.4.1 # homeassistant.components.uvc uvcclient==0.11.0 +# homeassistant.components.rdw +vehicle==0.1.0 + # homeassistant.components.velbus velbus-aio==2021.11.0 diff --git a/tests/components/rdw/__init__.py b/tests/components/rdw/__init__.py new file mode 100644 index 00000000000..6a628ecb94c --- /dev/null +++ b/tests/components/rdw/__init__.py @@ -0,0 +1 @@ +"""Tests for the RDW integration.""" diff --git a/tests/components/rdw/conftest.py b/tests/components/rdw/conftest.py new file mode 100644 index 00000000000..4be17f00264 --- /dev/null +++ b/tests/components/rdw/conftest.py @@ -0,0 +1,69 @@ +"""Fixtures for RDW integration tests.""" +from __future__ import annotations + +from collections.abc import Generator +from unittest.mock import MagicMock, patch + +import pytest +from vehicle import Vehicle + +from homeassistant.components.rdw.const import CONF_LICENSE_PLATE, DOMAIN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, load_fixture + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title="My Car", + domain=DOMAIN, + data={CONF_LICENSE_PLATE: "11ZKZ3"}, + unique_id="11ZKZ3", + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[None, None, None]: + """Mock setting up a config entry.""" + with patch("homeassistant.components.rdw.async_setup_entry", return_value=True): + yield + + +@pytest.fixture +def mock_rdw_config_flow() -> Generator[None, MagicMock, None]: + """Return a mocked RDW client.""" + with patch( + "homeassistant.components.rdw.config_flow.RDW", autospec=True + ) as rdw_mock: + rdw = rdw_mock.return_value + rdw.vehicle.return_value = Vehicle.parse_raw(load_fixture("rdw/11ZKZ3.json")) + yield rdw + + +@pytest.fixture +def mock_rdw(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]: + """Return a mocked WLED client.""" + fixture: str = "rdw/11ZKZ3.json" + if hasattr(request, "param") and request.param: + fixture = request.param + + vehicle = Vehicle.parse_raw(load_fixture(fixture)) + with patch("homeassistant.components.rdw.RDW", autospec=True) as rdw_mock: + rdw = rdw_mock.return_value + rdw.vehicle.return_value = vehicle + yield rdw + + +@pytest.fixture +async def init_integration( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_rdw: MagicMock +) -> MockConfigEntry: + """Set up the RDW integration for testing.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + return mock_config_entry diff --git a/tests/components/rdw/fixtures/11ZKZ3.json b/tests/components/rdw/fixtures/11ZKZ3.json new file mode 100644 index 00000000000..caaaf57c19b --- /dev/null +++ b/tests/components/rdw/fixtures/11ZKZ3.json @@ -0,0 +1,52 @@ +{ + "kenteken": "11ZKZ3", + "voertuigsoort": "Personenauto", + "merk": "SKODA", + "handelsbenaming": "CITIGO", + "vervaldatum_apk": "20220104", + "datum_tenaamstelling": "20211104", + "inrichting": "hatchback", + "aantal_zitplaatsen": "4", + "eerste_kleur": "GRIJS", + "tweede_kleur": "Niet geregistreerd", + "aantal_cilinders": "3", + "cilinderinhoud": "999", + "massa_ledig_voertuig": "840", + "toegestane_maximum_massa_voertuig": "1290", + "massa_rijklaar": "940", + "zuinigheidslabel": "A", + "datum_eerste_toelating": "20130104", + "datum_eerste_afgifte_nederland": "20130104", + "wacht_op_keuren": "Geen verstrekking in Open Data", + "catalogusprijs": "10697", + "wam_verzekerd": "Nee", + "aantal_deuren": "0", + "aantal_wielen": "4", + "afstand_hart_koppeling_tot_achterzijde_voertuig": "0", + "afstand_voorzijde_voertuig_tot_hart_koppeling": "0", + "lengte": "356", + "breedte": "0", + "europese_voertuigcategorie": "M1", + "plaats_chassisnummer": "r. motorruimte", + "technische_max_massa_voertuig": "1290", + "type": "AA", + "typegoedkeuringsnummer": "e13*2007/46*1169*05", + "variant": "ABCHYA", + "uitvoering": "FM5FM5CF0037MGVR2N1FA1SK", + "volgnummer_wijziging_eu_typegoedkeuring": "0", + "vermogen_massarijklaar": "0.05", + "wielbasis": "241", + "export_indicator": "Nee", + "openstaande_terugroepactie_indicator": "Nee", + "maximum_massa_samenstelling": "0", + "aantal_rolstoelplaatsen": "0", + "jaar_laatste_registratie_tellerstand": "2021", + "tellerstandoordeel": "Logisch", + "code_toelichting_tellerstandoordeel": "00", + "tenaamstellen_mogelijk": "Ja", + "api_gekentekende_voertuigen_assen": "https://opendata.rdw.nl/resource/3huj-srit.json", + "api_gekentekende_voertuigen_brandstof": "https://opendata.rdw.nl/resource/8ys7-d773.json", + "api_gekentekende_voertuigen_carrosserie": "https://opendata.rdw.nl/resource/vezc-m2t6.json", + "api_gekentekende_voertuigen_carrosserie_specifiek": "https://opendata.rdw.nl/resource/jhie-znh9.json", + "api_gekentekende_voertuigen_voertuigklasse": "https://opendata.rdw.nl/resource/kmfi-hrps.json" +} diff --git a/tests/components/rdw/test_config_flow.py b/tests/components/rdw/test_config_flow.py new file mode 100644 index 00000000000..20144768abe --- /dev/null +++ b/tests/components/rdw/test_config_flow.py @@ -0,0 +1,92 @@ +"""Tests for the RDW config flow.""" + +from unittest.mock import MagicMock + +from vehicle.exceptions import RDWConnectionError, RDWUnknownLicensePlateError + +from homeassistant.components.rdw.const import CONF_LICENSE_PLATE, DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM + + +async def test_full_user_flow( + hass: HomeAssistant, mock_rdw_config_flow: MagicMock, mock_setup_entry: MagicMock +) -> None: + """Test the full user configuration flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LICENSE_PLATE: "11-ZKZ-3", + }, + ) + + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("title") == "11-ZKZ-3" + assert result2.get("data") == {CONF_LICENSE_PLATE: "11ZKZ3"} + + +async def test_full_flow_with_authentication_error( + hass: HomeAssistant, mock_rdw_config_flow: MagicMock, mock_setup_entry: MagicMock +) -> None: + """Test the full user configuration flow with incorrect license plate. + + This tests tests a full config flow, with a case the user enters an invalid + license plate, but recover by entering the correct one. + """ + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == SOURCE_USER + assert "flow_id" in result + + mock_rdw_config_flow.vehicle.side_effect = RDWUnknownLicensePlateError + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_LICENSE_PLATE: "0001TJ", + }, + ) + + assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("step_id") == SOURCE_USER + assert result2.get("errors") == {"base": "unknown_license_plate"} + assert "flow_id" in result2 + + mock_rdw_config_flow.vehicle.side_effect = None + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + user_input={ + CONF_LICENSE_PLATE: "11-ZKZ-3", + }, + ) + + assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("title") == "11-ZKZ-3" + assert result3.get("data") == {CONF_LICENSE_PLATE: "11ZKZ3"} + + +async def test_connection_error( + hass: HomeAssistant, mock_rdw_config_flow: MagicMock +) -> None: + """Test API connection error.""" + mock_rdw_config_flow.vehicle.side_effect = RDWConnectionError + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_LICENSE_PLATE: "0001TJ"}, + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("errors") == {"base": "cannot_connect"} diff --git a/tests/components/rdw/test_init.py b/tests/components/rdw/test_init.py new file mode 100644 index 00000000000..b31b0aa8d81 --- /dev/null +++ b/tests/components/rdw/test_init.py @@ -0,0 +1,45 @@ +"""Tests for the RDW integration.""" +from unittest.mock import AsyncMock, MagicMock, patch + +from homeassistant.components.rdw.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +async def test_load_unload_config_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_rdw: AsyncMock, +) -> None: + """Test the RDW configuration entry loading/unloading.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert not hass.data.get(DOMAIN) + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +@patch( + "homeassistant.components.rdw.RDW.vehicle", + side_effect=RuntimeError, +) +async def test_config_entry_not_ready( + mock_request: MagicMock, + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the RDW configuration entry not ready.""" + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_request.call_count == 1 + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/rdw/test_sensor.py b/tests/components/rdw/test_sensor.py new file mode 100644 index 00000000000..c67a7459b0d --- /dev/null +++ b/tests/components/rdw/test_sensor.py @@ -0,0 +1,61 @@ +"""Tests for the sensors provided by the RDW integration.""" +from homeassistant.components.rdw.const import DOMAIN, ENTRY_TYPE_SERVICE +from homeassistant.components.sensor import ATTR_STATE_CLASS +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, + ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_DATE, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.common import MockConfigEntry + + +async def test_vehicle_sensors( + hass: HomeAssistant, + init_integration: MockConfigEntry, +) -> None: + """Test the RDW vehicle sensors.""" + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + state = hass.states.get("sensor.apk_expiration") + entry = entity_registry.async_get("sensor.apk_expiration") + assert entry + assert state + assert entry.unique_id == "11ZKZ3_apk_expiration" + assert state.state == "2022-01-04" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "APK Expiration" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_DATE + assert ATTR_ICON not in state.attributes + assert ATTR_STATE_CLASS not in state.attributes + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + + state = hass.states.get("sensor.name_registration_date") + entry = entity_registry.async_get("sensor.name_registration_date") + assert entry + assert state + assert entry.unique_id == "11ZKZ3_name_registration_date" + assert state.state == "2021-11-04" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Name Registration Date" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_DATE + assert ATTR_ICON not in state.attributes + assert ATTR_STATE_CLASS not in state.attributes + assert ATTR_UNIT_OF_MEASUREMENT not in state.attributes + + assert entry.device_id + device_entry = device_registry.async_get(entry.device_id) + assert device_entry + assert device_entry.identifiers == {(DOMAIN, "11ZKZ3")} + assert device_entry.manufacturer == "Skoda" + assert device_entry.name == "Skoda: 11ZKZ3" + assert device_entry.entry_type == ENTRY_TYPE_SERVICE + assert device_entry.model == "Citigo" + assert ( + device_entry.configuration_url + == "https://ovi.rdw.nl/default.aspx?kenteken=11ZKZ3" + ) + assert not device_entry.sw_version