mirror of
https://github.com/home-assistant/core.git
synced 2025-07-25 14:17:45 +00:00
Add RDW Vehicle information integration (#59240)
This commit is contained in:
parent
fc7d4ed118
commit
fdf1bfa140
@ -96,6 +96,7 @@ homeassistant.components.persistent_notification.*
|
|||||||
homeassistant.components.pi_hole.*
|
homeassistant.components.pi_hole.*
|
||||||
homeassistant.components.proximity.*
|
homeassistant.components.proximity.*
|
||||||
homeassistant.components.rainmachine.*
|
homeassistant.components.rainmachine.*
|
||||||
|
homeassistant.components.rdw.*
|
||||||
homeassistant.components.recollect_waste.*
|
homeassistant.components.recollect_waste.*
|
||||||
homeassistant.components.recorder.purge
|
homeassistant.components.recorder.purge
|
||||||
homeassistant.components.recorder.repack
|
homeassistant.components.recorder.repack
|
||||||
|
@ -425,6 +425,7 @@ homeassistant/components/raincloud/* @vanstinator
|
|||||||
homeassistant/components/rainforest_eagle/* @gtdiehl @jcalbert
|
homeassistant/components/rainforest_eagle/* @gtdiehl @jcalbert
|
||||||
homeassistant/components/rainmachine/* @bachya
|
homeassistant/components/rainmachine/* @bachya
|
||||||
homeassistant/components/random/* @fabaff
|
homeassistant/components/random/* @fabaff
|
||||||
|
homeassistant/components/rdw/* @frenck
|
||||||
homeassistant/components/recollect_waste/* @bachya
|
homeassistant/components/recollect_waste/* @bachya
|
||||||
homeassistant/components/recorder/* @home-assistant/core
|
homeassistant/components/recorder/* @home-assistant/core
|
||||||
homeassistant/components/rejseplanen/* @DarkFox
|
homeassistant/components/rejseplanen/* @DarkFox
|
||||||
|
42
homeassistant/components/rdw/__init__.py
Normal file
42
homeassistant/components/rdw/__init__.py
Normal file
@ -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
|
56
homeassistant/components/rdw/config_flow.py
Normal file
56
homeassistant/components/rdw/config_flow.py
Normal file
@ -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,
|
||||||
|
)
|
14
homeassistant/components/rdw/const.py
Normal file
14
homeassistant/components/rdw/const.py
Normal file
@ -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"
|
10
homeassistant/components/rdw/manifest.json
Normal file
10
homeassistant/components/rdw/manifest.json
Normal file
@ -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"
|
||||||
|
}
|
103
homeassistant/components/rdw/sensor.py
Normal file
103
homeassistant/components/rdw/sensor.py
Normal file
@ -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)
|
15
homeassistant/components/rdw/strings.json
Normal file
15
homeassistant/components/rdw/strings.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
homeassistant/components/rdw/translations/en.json
Normal file
15
homeassistant/components/rdw/translations/en.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "Failed to connect",
|
||||||
|
"unknown_license_plate": "Unknown license plate"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"license_plate": "License plate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -235,6 +235,7 @@ FLOWS = [
|
|||||||
"rachio",
|
"rachio",
|
||||||
"rainforest_eagle",
|
"rainforest_eagle",
|
||||||
"rainmachine",
|
"rainmachine",
|
||||||
|
"rdw",
|
||||||
"recollect_waste",
|
"recollect_waste",
|
||||||
"renault",
|
"renault",
|
||||||
"rfxtrx",
|
"rfxtrx",
|
||||||
|
11
mypy.ini
11
mypy.ini
@ -1067,6 +1067,17 @@ no_implicit_optional = true
|
|||||||
warn_return_any = true
|
warn_return_any = true
|
||||||
warn_unreachable = 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.*]
|
[mypy-homeassistant.components.recollect_waste.*]
|
||||||
check_untyped_defs = true
|
check_untyped_defs = true
|
||||||
disallow_incomplete_defs = true
|
disallow_incomplete_defs = true
|
||||||
|
@ -2365,6 +2365,9 @@ uvcclient==0.11.0
|
|||||||
# homeassistant.components.vallox
|
# homeassistant.components.vallox
|
||||||
vallox-websocket-api==2.8.1
|
vallox-websocket-api==2.8.1
|
||||||
|
|
||||||
|
# homeassistant.components.rdw
|
||||||
|
vehicle==0.1.0
|
||||||
|
|
||||||
# homeassistant.components.velbus
|
# homeassistant.components.velbus
|
||||||
velbus-aio==2021.11.0
|
velbus-aio==2021.11.0
|
||||||
|
|
||||||
|
@ -1372,6 +1372,9 @@ url-normalize==1.4.1
|
|||||||
# homeassistant.components.uvc
|
# homeassistant.components.uvc
|
||||||
uvcclient==0.11.0
|
uvcclient==0.11.0
|
||||||
|
|
||||||
|
# homeassistant.components.rdw
|
||||||
|
vehicle==0.1.0
|
||||||
|
|
||||||
# homeassistant.components.velbus
|
# homeassistant.components.velbus
|
||||||
velbus-aio==2021.11.0
|
velbus-aio==2021.11.0
|
||||||
|
|
||||||
|
1
tests/components/rdw/__init__.py
Normal file
1
tests/components/rdw/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Tests for the RDW integration."""
|
69
tests/components/rdw/conftest.py
Normal file
69
tests/components/rdw/conftest.py
Normal file
@ -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
|
52
tests/components/rdw/fixtures/11ZKZ3.json
Normal file
52
tests/components/rdw/fixtures/11ZKZ3.json
Normal file
@ -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"
|
||||||
|
}
|
92
tests/components/rdw/test_config_flow.py
Normal file
92
tests/components/rdw/test_config_flow.py
Normal file
@ -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"}
|
45
tests/components/rdw/test_init.py
Normal file
45
tests/components/rdw/test_init.py
Normal file
@ -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
|
61
tests/components/rdw/test_sensor.py
Normal file
61
tests/components/rdw/test_sensor.py
Normal file
@ -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
|
Loading…
x
Reference in New Issue
Block a user