diff --git a/homeassistant/components/renault/__init__.py b/homeassistant/components/renault/__init__.py index 8f9ab9765cdf..c8f30aab7a6c 100644 --- a/homeassistant/components/renault/__init__.py +++ b/homeassistant/components/renault/__init__.py @@ -10,7 +10,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.typing import ConfigType -from .const import CONF_LOCALE, DOMAIN, PLATFORMS +from .const import DOMAIN, PLATFORMS, RenaultConfigurationKeys from .renault_hub import RenaultHub from .services import async_setup_services @@ -28,7 +28,7 @@ async def async_setup_entry( hass: HomeAssistant, config_entry: RenaultConfigEntry ) -> bool: """Load a config entry.""" - renault_hub = RenaultHub(hass, config_entry.data[CONF_LOCALE]) + renault_hub = RenaultHub(hass, config_entry.data[RenaultConfigurationKeys.LOCALE]) try: await renault_hub.async_initialise(config_entry) except NotAuthenticatedException as exc: diff --git a/homeassistant/components/renault/config_flow.py b/homeassistant/components/renault/config_flow.py index 53e2798cc9bb..a3d8f73dddf7 100644 --- a/homeassistant/components/renault/config_flow.py +++ b/homeassistant/components/renault/config_flow.py @@ -14,21 +14,22 @@ from homeassistant.config_entries import ( ConfigFlow, ConfigFlowResult, ) -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from .const import CONF_KAMEREON_ACCOUNT_ID, CONF_LOCALE, CONF_LOGIN_TOKEN, DOMAIN +from .const import DOMAIN, RenaultConfigurationKeys from .renault_hub import RenaultHub _LOGGER = logging.getLogger(__name__) USER_SCHEMA = vol.Schema( { - vol.Required(CONF_LOCALE): vol.In(AVAILABLE_LOCALES.keys()), - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str, + vol.Required(RenaultConfigurationKeys.LOCALE.value): vol.In( + AVAILABLE_LOCALES.keys() + ), + vol.Required(RenaultConfigurationKeys.USERNAME.value): str, + vol.Required(RenaultConfigurationKeys.PASSWORD.value): str, } ) -REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) +REAUTH_SCHEMA = vol.Schema({vol.Required(RenaultConfigurationKeys.PASSWORD.value): str}) class RenaultFlowHandler(ConfigFlow, domain=DOMAIN): @@ -50,13 +51,14 @@ class RenaultFlowHandler(ConfigFlow, domain=DOMAIN): errors: dict[str, str] = {} suggested_values: Mapping[str, Any] | None = None if user_input: - locale = user_input[CONF_LOCALE] + locale = user_input[RenaultConfigurationKeys.LOCALE] self.renault_config.update(user_input) self.renault_config.update(AVAILABLE_LOCALES[locale]) self.renault_hub = RenaultHub(self.hass, locale) try: login_success = await self.renault_hub.attempt_login( - user_input[CONF_USERNAME], user_input[CONF_PASSWORD] + user_input[RenaultConfigurationKeys.USERNAME], + user_input[RenaultConfigurationKeys.PASSWORD], ) except aiohttp.ClientConnectionError, GigyaException: errors["base"] = "cannot_connect" @@ -67,7 +69,9 @@ class RenaultFlowHandler(ConfigFlow, domain=DOMAIN): if login_success: if TYPE_CHECKING: assert self.renault_hub.login_token - self.renault_config[CONF_LOGIN_TOKEN] = self.renault_hub.login_token + self.renault_config[RenaultConfigurationKeys.LOGIN_TOKEN] = ( + self.renault_hub.login_token + ) return await self.async_step_kamereon() errors["base"] = "invalid_credentials" suggested_values = user_input @@ -87,7 +91,9 @@ class RenaultFlowHandler(ConfigFlow, domain=DOMAIN): ) -> ConfigFlowResult: """Select Kamereon account.""" if user_input: - await self.async_set_unique_id(user_input[CONF_KAMEREON_ACCOUNT_ID]) + await self.async_set_unique_id( + user_input[RenaultConfigurationKeys.KAMEREON_ACCOUNT_ID] + ) if self.source == SOURCE_RECONFIGURE: self._abort_if_unique_id_mismatch() self.renault_config.update(user_input) @@ -100,7 +106,8 @@ class RenaultFlowHandler(ConfigFlow, domain=DOMAIN): self.renault_config.update(user_input) return self.async_create_entry( - title=user_input[CONF_KAMEREON_ACCOUNT_ID], data=self.renault_config + title=user_input[RenaultConfigurationKeys.KAMEREON_ACCOUNT_ID], + data=self.renault_config, ) accounts = await self.renault_hub.get_account_ids() @@ -108,13 +115,17 @@ class RenaultFlowHandler(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="kamereon_no_account") if len(accounts) == 1: return await self.async_step_kamereon( - user_input={CONF_KAMEREON_ACCOUNT_ID: accounts[0]} + user_input={RenaultConfigurationKeys.KAMEREON_ACCOUNT_ID: accounts[0]} ) return self.async_show_form( step_id="kamereon", data_schema=vol.Schema( - {vol.Required(CONF_KAMEREON_ACCOUNT_ID): vol.In(accounts)} + { + vol.Required( + RenaultConfigurationKeys.KAMEREON_ACCOUNT_ID.value + ): vol.In(accounts) + } ), ) @@ -132,17 +143,22 @@ class RenaultFlowHandler(ConfigFlow, domain=DOMAIN): reauth_entry = self._get_reauth_entry() if user_input: # Check credentials - self.renault_hub = RenaultHub(self.hass, reauth_entry.data[CONF_LOCALE]) + self.renault_hub = RenaultHub( + self.hass, reauth_entry.data[RenaultConfigurationKeys.LOCALE] + ) if await self.renault_hub.attempt_login( - reauth_entry.data[CONF_USERNAME], user_input[CONF_PASSWORD] + reauth_entry.data[RenaultConfigurationKeys.USERNAME], + user_input[RenaultConfigurationKeys.PASSWORD], ): if TYPE_CHECKING: assert self.renault_hub.login_token return self.async_update_reload_and_abort( reauth_entry, data_updates={ - CONF_PASSWORD: user_input[CONF_PASSWORD], - CONF_LOGIN_TOKEN: self.renault_hub.login_token, + RenaultConfigurationKeys.PASSWORD: user_input[ + RenaultConfigurationKeys.PASSWORD + ], + RenaultConfigurationKeys.LOGIN_TOKEN: self.renault_hub.login_token, }, ) errors = {"base": "invalid_credentials"} @@ -151,7 +167,11 @@ class RenaultFlowHandler(ConfigFlow, domain=DOMAIN): step_id="reauth_confirm", data_schema=REAUTH_SCHEMA, errors=errors, - description_placeholders={CONF_USERNAME: reauth_entry.data[CONF_USERNAME]}, + description_placeholders={ + RenaultConfigurationKeys.USERNAME: reauth_entry.data[ + RenaultConfigurationKeys.USERNAME + ] + }, ) async def async_step_reconfigure( diff --git a/homeassistant/components/renault/const.py b/homeassistant/components/renault/const.py index aeb0799fea31..6d27d44b9ab9 100644 --- a/homeassistant/components/renault/const.py +++ b/homeassistant/components/renault/const.py @@ -1,12 +1,21 @@ """Constants for the Renault component.""" +from enum import StrEnum + from homeassistant.const import Platform DOMAIN = "renault" -CONF_LOCALE = "locale" -CONF_KAMEREON_ACCOUNT_ID = "kamereon_account_id" -CONF_LOGIN_TOKEN = "login_token" + +class RenaultConfigurationKeys(StrEnum): + """Configuration keys.""" + + LOCALE = "locale" + KAMEREON_ACCOUNT_ID = "kamereon_account_id" + LOGIN_TOKEN = "login_token" + USERNAME = "username" + PASSWORD = "password" + # normal number of allowed calls per hour to the API # for a single car and the 7 coordinator, it is a scan every 7mn diff --git a/homeassistant/components/renault/diagnostics.py b/homeassistant/components/renault/diagnostics.py index b47aa4296731..20f179bbd3b8 100644 --- a/homeassistant/components/renault/diagnostics.py +++ b/homeassistant/components/renault/diagnostics.py @@ -3,19 +3,18 @@ from typing import Any from homeassistant.components.diagnostics import async_redact_data -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntry from . import RenaultConfigEntry -from .const import CONF_KAMEREON_ACCOUNT_ID, CONF_LOGIN_TOKEN +from .const import RenaultConfigurationKeys from .renault_vehicle import RenaultVehicleProxy TO_REDACT = { - CONF_KAMEREON_ACCOUNT_ID, - CONF_LOGIN_TOKEN, - CONF_PASSWORD, - CONF_USERNAME, + RenaultConfigurationKeys.KAMEREON_ACCOUNT_ID, + RenaultConfigurationKeys.LOGIN_TOKEN, + RenaultConfigurationKeys.PASSWORD, + RenaultConfigurationKeys.USERNAME, "radioCode", "registrationNumber", "vin", diff --git a/homeassistant/components/renault/renault_hub.py b/homeassistant/components/renault/renault_hub.py index 9b8f3d9cfd28..d9beb1d5301e 100644 --- a/homeassistant/components/renault/renault_hub.py +++ b/homeassistant/components/renault/renault_hub.py @@ -3,6 +3,7 @@ import asyncio from datetime import timedelta import logging +from time import time from typing import TYPE_CHECKING from renault_api.exceptions import NotAuthenticatedException @@ -17,27 +18,22 @@ from homeassistant.const import ( ATTR_MODEL, ATTR_MODEL_ID, ATTR_NAME, - CONF_PASSWORD, - CONF_USERNAME, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_get_clientsession -if TYPE_CHECKING: - from . import RenaultConfigEntry - -from time import time - from .const import ( - CONF_KAMEREON_ACCOUNT_ID, - CONF_LOGIN_TOKEN, COOLING_UPDATES_SECONDS, MAX_CALLS_PER_HOURS, + RenaultConfigurationKeys, ) from .renault_vehicle import COORDINATORS, RenaultVehicleProxy +if TYPE_CHECKING: + from . import RenaultConfigEntry + LOGGER = logging.getLogger(__name__) @@ -106,20 +102,26 @@ class RenaultHub: async def async_initialise(self, config_entry: RenaultConfigEntry) -> None: """Set up proxy.""" # Reuse the stored login token, or fall back to a password login. - if login_token := config_entry.data.get(CONF_LOGIN_TOKEN): + if login_token := config_entry.data.get(RenaultConfigurationKeys.LOGIN_TOKEN): self._client.session.set_login_token(login_token) elif await self.attempt_login( - config_entry.data[CONF_USERNAME], config_entry.data[CONF_PASSWORD] + config_entry.data[RenaultConfigurationKeys.USERNAME], + config_entry.data[RenaultConfigurationKeys.PASSWORD], ): # Persist the login token so the next setup can skip the password. self._hass.config_entries.async_update_entry( config_entry, - data={**config_entry.data, CONF_LOGIN_TOKEN: self.login_token}, + data={ + **config_entry.data, + RenaultConfigurationKeys.LOGIN_TOKEN: self.login_token, + }, ) else: raise NotAuthenticatedException - account_id: str = config_entry.data[CONF_KAMEREON_ACCOUNT_ID] + account_id: str = config_entry.data[ + RenaultConfigurationKeys.KAMEREON_ACCOUNT_ID + ] self._account = await self._client.get_api_account(account_id) vehicle_links = await _get_filtered_vehicles(self._account) diff --git a/homeassistant/components/renault/services.py b/homeassistant/components/renault/services.py index b1e9cda56f7a..0a219b8accb3 100644 --- a/homeassistant/components/renault/services.py +++ b/homeassistant/components/renault/services.py @@ -1,12 +1,12 @@ """Support for Renault services.""" from datetime import datetime +from enum import StrEnum import logging from typing import TYPE_CHECKING, Any import voluptuous as vol -from homeassistant.const import ATTR_TEMPERATURE from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import config_validation as cv, device_registry as dr @@ -19,24 +19,30 @@ if TYPE_CHECKING: LOGGER = logging.getLogger(__name__) -ATTR_SCHEDULES = "schedules" -ATTR_VEHICLE = "vehicle" -ATTR_WHEN = "when" + +class RenaultServiceArgument(StrEnum): + """Service argument names.""" + + SCHEDULES = "schedules" + TEMPERATURE = "temperature" + VEHICLE = "vehicle" + WHEN = "when" + SERVICE_VEHICLE_SCHEMA = vol.Schema( { - vol.Required(ATTR_VEHICLE): cv.string, + vol.Required(RenaultServiceArgument.VEHICLE.value): cv.string, } ) SERVICE_AC_START_SCHEMA = SERVICE_VEHICLE_SCHEMA.extend( { - vol.Required(ATTR_TEMPERATURE): cv.positive_float, - vol.Optional(ATTR_WHEN): cv.datetime, + vol.Required(RenaultServiceArgument.TEMPERATURE.value): cv.positive_float, + vol.Optional(RenaultServiceArgument.WHEN.value): cv.datetime, } ) SERVICE_CHARGE_START_SCHEMA = SERVICE_VEHICLE_SCHEMA.extend( { - vol.Optional(ATTR_WHEN): cv.datetime, + vol.Optional(RenaultServiceArgument.WHEN.value): cv.datetime, } ) SERVICE_CHARGE_SET_SCHEDULE_DAY_SCHEMA = vol.Schema( @@ -62,7 +68,7 @@ SERVICE_CHARGE_SET_SCHEDULE_SCHEMA = vol.Schema( ) SERVICE_CHARGE_SET_SCHEDULES_SCHEMA = SERVICE_VEHICLE_SCHEMA.extend( { - vol.Required(ATTR_SCHEDULES): vol.All( + vol.Required(RenaultServiceArgument.SCHEDULES.value): vol.All( cv.ensure_list, [SERVICE_CHARGE_SET_SCHEDULE_SCHEMA] ), } @@ -89,7 +95,7 @@ SERVICE_AC_SET_SCHEDULE_SCHEMA = vol.Schema( ) SERVICE_AC_SET_SCHEDULES_SCHEMA = SERVICE_VEHICLE_SCHEMA.extend( { - vol.Required(ATTR_SCHEDULES): vol.All( + vol.Required(RenaultServiceArgument.SCHEDULES.value): vol.All( cv.ensure_list, [SERVICE_AC_SET_SCHEDULE_SCHEMA] ), } @@ -107,8 +113,8 @@ async def ac_cancel(service_call: ServiceCall) -> None: async def ac_start(service_call: ServiceCall) -> None: """Start A/C.""" - temperature: float = service_call.data[ATTR_TEMPERATURE] - when: datetime | None = service_call.data.get(ATTR_WHEN) + temperature: float = service_call.data[RenaultServiceArgument.TEMPERATURE] + when: datetime | None = service_call.data.get(RenaultServiceArgument.WHEN) proxy = get_vehicle_proxy(service_call) LOGGER.debug("A/C start attempt: %s / %s", temperature, when) @@ -118,7 +124,7 @@ async def ac_start(service_call: ServiceCall) -> None: async def charge_start(service_call: ServiceCall) -> None: """Start Charging with optional delay.""" - when: datetime | None = service_call.data.get(ATTR_WHEN) + when: datetime | None = service_call.data.get(RenaultServiceArgument.WHEN) proxy = get_vehicle_proxy(service_call) LOGGER.debug("Charge start attempt, when: %s", when) @@ -128,7 +134,9 @@ async def charge_start(service_call: ServiceCall) -> None: async def charge_set_schedules(service_call: ServiceCall) -> None: """Set charge schedules.""" - schedules: list[dict[str, Any]] = service_call.data[ATTR_SCHEDULES] + schedules: list[dict[str, Any]] = service_call.data[ + RenaultServiceArgument.SCHEDULES + ] proxy = get_vehicle_proxy(service_call) charge_schedules = await proxy.get_charging_settings() for schedule in schedules: @@ -147,7 +155,9 @@ async def charge_set_schedules(service_call: ServiceCall) -> None: async def ac_set_schedules(service_call: ServiceCall) -> None: """Set A/C schedules.""" - schedules: list[dict[str, Any]] = service_call.data[ATTR_SCHEDULES] + schedules: list[dict[str, Any]] = service_call.data[ + RenaultServiceArgument.SCHEDULES + ] proxy = get_vehicle_proxy(service_call) hvac_schedules = await proxy.get_hvac_settings() @@ -168,7 +178,7 @@ async def ac_set_schedules(service_call: ServiceCall) -> None: def get_vehicle_proxy(service_call: ServiceCall) -> RenaultVehicleProxy: """Get vehicle from service_call data.""" device_registry = dr.async_get(service_call.hass) - device_id = service_call.data[ATTR_VEHICLE] + device_id = service_call.data[RenaultServiceArgument.VEHICLE] device_entry = device_registry.async_get(device_id) if device_entry is None: raise ServiceValidationError( diff --git a/tests/components/renault/const.py b/tests/components/renault/const.py index 8a17bf201fbf..35f6daf504cb 100644 --- a/tests/components/renault/const.py +++ b/tests/components/renault/const.py @@ -1,22 +1,17 @@ """Constants for the Renault integration tests.""" -from homeassistant.components.renault.const import ( - CONF_KAMEREON_ACCOUNT_ID, - CONF_LOCALE, - CONF_LOGIN_TOKEN, -) -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.components.renault.const import RenaultConfigurationKeys MOCK_ACCOUNT_ID = "account_id_1" MOCK_LOGIN_TOKEN = "sample-login-token" # Mock config data to be used across multiple tests MOCK_CONFIG = { - CONF_USERNAME: "email@test.com", - CONF_PASSWORD: "test", - CONF_LOGIN_TOKEN: MOCK_LOGIN_TOKEN, - CONF_KAMEREON_ACCOUNT_ID: MOCK_ACCOUNT_ID, - CONF_LOCALE: "fr_FR", + RenaultConfigurationKeys.USERNAME: "email@test.com", + RenaultConfigurationKeys.PASSWORD: "test", + RenaultConfigurationKeys.LOGIN_TOKEN: MOCK_LOGIN_TOKEN, + RenaultConfigurationKeys.KAMEREON_ACCOUNT_ID: MOCK_ACCOUNT_ID, + RenaultConfigurationKeys.LOCALE: "fr_FR", } MOCK_VEHICLES = { diff --git a/tests/components/renault/test_config_flow.py b/tests/components/renault/test_config_flow.py index 09b4a94b1ad5..fe98ee33de11 100644 --- a/tests/components/renault/test_config_flow.py +++ b/tests/components/renault/test_config_flow.py @@ -10,13 +10,7 @@ from renault_api.renault_account import RenaultAccount from renault_api.renault_session import RenaultSession from homeassistant import config_entries -from homeassistant.components.renault.const import ( - CONF_KAMEREON_ACCOUNT_ID, - CONF_LOCALE, - CONF_LOGIN_TOKEN, - DOMAIN, -) -from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME +from homeassistant.components.renault.const import DOMAIN, RenaultConfigurationKeys from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import aiohttp_client @@ -66,9 +60,9 @@ async def test_config_flow_single_account( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ - CONF_LOCALE: "fr_FR", - CONF_USERNAME: "email@test.com", - CONF_PASSWORD: "test", + RenaultConfigurationKeys.LOCALE: "fr_FR", + RenaultConfigurationKeys.USERNAME: "email@test.com", + RenaultConfigurationKeys.PASSWORD: "test", }, ) @@ -77,9 +71,18 @@ async def test_config_flow_single_account( assert result["errors"] == {"base": error} data_schema = result["data_schema"].schema - assert get_schema_suggested_value(data_schema, CONF_LOCALE) == "fr_FR" - assert get_schema_suggested_value(data_schema, CONF_USERNAME) == "email@test.com" - assert get_schema_suggested_value(data_schema, CONF_PASSWORD) == "test" + assert ( + get_schema_suggested_value(data_schema, RenaultConfigurationKeys.LOCALE) + == "fr_FR" + ) + assert ( + get_schema_suggested_value(data_schema, RenaultConfigurationKeys.USERNAME) + == "email@test.com" + ) + assert ( + get_schema_suggested_value(data_schema, RenaultConfigurationKeys.PASSWORD) + == "test" + ) renault_account = AsyncMock() type(renault_account).account_id = PropertyMock(return_value="account_id_1") @@ -104,19 +107,21 @@ async def test_config_flow_single_account( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ - CONF_LOCALE: "fr_FR", - CONF_USERNAME: "email@test.com", - CONF_PASSWORD: "test", + RenaultConfigurationKeys.LOCALE: "fr_FR", + RenaultConfigurationKeys.USERNAME: "email@test.com", + RenaultConfigurationKeys.PASSWORD: "test", }, ) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "account_id_1" - assert result["data"][CONF_USERNAME] == "email@test.com" - assert result["data"][CONF_PASSWORD] == "test" - assert result["data"][CONF_LOGIN_TOKEN] == MOCK_LOGIN_TOKEN - assert result["data"][CONF_KAMEREON_ACCOUNT_ID] == "account_id_1" - assert result["data"][CONF_LOCALE] == "fr_FR" + assert result["data"][RenaultConfigurationKeys.USERNAME] == "email@test.com" + assert result["data"][RenaultConfigurationKeys.PASSWORD] == "test" + assert result["data"][RenaultConfigurationKeys.LOGIN_TOKEN] == MOCK_LOGIN_TOKEN + assert ( + result["data"][RenaultConfigurationKeys.KAMEREON_ACCOUNT_ID] == "account_id_1" + ) + assert result["data"][RenaultConfigurationKeys.LOCALE] == "fr_FR" assert result["context"]["unique_id"] == "account_id_1" assert len(mock_setup_entry.mock_calls) == 1 @@ -147,9 +152,9 @@ async def test_config_flow_no_account( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ - CONF_LOCALE: "fr_FR", - CONF_USERNAME: "email@test.com", - CONF_PASSWORD: "test", + RenaultConfigurationKeys.LOCALE: "fr_FR", + RenaultConfigurationKeys.USERNAME: "email@test.com", + RenaultConfigurationKeys.PASSWORD: "test", }, ) @@ -200,9 +205,9 @@ async def test_config_flow_multiple_accounts( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ - CONF_LOCALE: "fr_FR", - CONF_USERNAME: "email@test.com", - CONF_PASSWORD: "test", + RenaultConfigurationKeys.LOCALE: "fr_FR", + RenaultConfigurationKeys.USERNAME: "email@test.com", + RenaultConfigurationKeys.PASSWORD: "test", }, ) @@ -212,15 +217,17 @@ async def test_config_flow_multiple_accounts( # Account selected result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={CONF_KAMEREON_ACCOUNT_ID: "account_id_2"}, + user_input={RenaultConfigurationKeys.KAMEREON_ACCOUNT_ID: "account_id_2"}, ) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "account_id_2" - assert result["data"][CONF_USERNAME] == "email@test.com" - assert result["data"][CONF_PASSWORD] == "test" - assert result["data"][CONF_LOGIN_TOKEN] == MOCK_LOGIN_TOKEN - assert result["data"][CONF_KAMEREON_ACCOUNT_ID] == "account_id_2" - assert result["data"][CONF_LOCALE] == "fr_FR" + assert result["data"][RenaultConfigurationKeys.USERNAME] == "email@test.com" + assert result["data"][RenaultConfigurationKeys.PASSWORD] == "test" + assert result["data"][RenaultConfigurationKeys.LOGIN_TOKEN] == MOCK_LOGIN_TOKEN + assert ( + result["data"][RenaultConfigurationKeys.KAMEREON_ACCOUNT_ID] == "account_id_2" + ) + assert result["data"][RenaultConfigurationKeys.LOCALE] == "fr_FR" assert result["context"]["unique_id"] == "account_id_2" assert len(mock_setup_entry.mock_calls) == 1 @@ -264,9 +271,9 @@ async def test_config_flow_duplicate( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ - CONF_LOCALE: "fr_FR", - CONF_USERNAME: "email@test.com", - CONF_PASSWORD: "test", + RenaultConfigurationKeys.LOCALE: "fr_FR", + RenaultConfigurationKeys.USERNAME: "email@test.com", + RenaultConfigurationKeys.PASSWORD: "test", }, ) @@ -285,8 +292,8 @@ async def test_reauth(hass: HomeAssistant, config_entry: MockConfigEntry) -> Non assert result["type"] is FlowResultType.FORM assert result["description_placeholders"] == { - CONF_NAME: "Mock Title", - CONF_USERNAME: "email@test.com", + "name": "Mock Title", + RenaultConfigurationKeys.USERNAME: "email@test.com", } assert result["errors"] == {} @@ -297,13 +304,13 @@ async def test_reauth(hass: HomeAssistant, config_entry: MockConfigEntry) -> Non ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={CONF_PASSWORD: "any"}, + user_input={RenaultConfigurationKeys.PASSWORD: "any"}, ) assert result2["type"] is FlowResultType.FORM assert result2["description_placeholders"] == { - CONF_NAME: "Mock Title", - CONF_USERNAME: "email@test.com", + "name": "Mock Title", + RenaultConfigurationKeys.USERNAME: "email@test.com", } assert result2["errors"] == {"base": "invalid_credentials"} @@ -315,15 +322,15 @@ async def test_reauth(hass: HomeAssistant, config_entry: MockConfigEntry) -> Non ): result3 = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={CONF_PASSWORD: "any"}, + user_input={RenaultConfigurationKeys.PASSWORD: "any"}, ) assert result3["type"] is FlowResultType.ABORT assert result3["reason"] == "reauth_successful" - assert config_entry.data[CONF_USERNAME] == "email@test.com" - assert config_entry.data[CONF_PASSWORD] == "any" - assert config_entry.data[CONF_LOGIN_TOKEN] == MOCK_LOGIN_TOKEN + assert config_entry.data[RenaultConfigurationKeys.USERNAME] == "email@test.com" + assert config_entry.data[RenaultConfigurationKeys.PASSWORD] == "any" + assert config_entry.data[RenaultConfigurationKeys.LOGIN_TOKEN] == MOCK_LOGIN_TOKEN async def test_reconfigure( @@ -338,9 +345,18 @@ async def test_reconfigure( assert not result["errors"] data_schema = result["data_schema"].schema - assert get_schema_suggested_value(data_schema, CONF_LOCALE) == "fr_FR" - assert get_schema_suggested_value(data_schema, CONF_USERNAME) == "email@test.com" - assert get_schema_suggested_value(data_schema, CONF_PASSWORD) == "test" + assert ( + get_schema_suggested_value(data_schema, RenaultConfigurationKeys.LOCALE) + == "fr_FR" + ) + assert ( + get_schema_suggested_value(data_schema, RenaultConfigurationKeys.USERNAME) + == "email@test.com" + ) + assert ( + get_schema_suggested_value(data_schema, RenaultConfigurationKeys.PASSWORD) + == "test" + ) renault_account = AsyncMock() type(renault_account).account_id = PropertyMock(return_value="account_id_1") @@ -365,20 +381,23 @@ async def test_reconfigure( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ - CONF_LOCALE: "fr_FR", - CONF_USERNAME: "email2@test.com", - CONF_PASSWORD: "test2", + RenaultConfigurationKeys.LOCALE: "fr_FR", + RenaultConfigurationKeys.USERNAME: "email2@test.com", + RenaultConfigurationKeys.PASSWORD: "test2", }, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" - assert config_entry.data[CONF_USERNAME] == "email2@test.com" - assert config_entry.data[CONF_PASSWORD] == "test2" - assert config_entry.data[CONF_LOGIN_TOKEN] == MOCK_LOGIN_TOKEN - assert config_entry.data[CONF_KAMEREON_ACCOUNT_ID] == "account_id_1" - assert config_entry.data[CONF_LOCALE] == "fr_FR" + assert config_entry.data[RenaultConfigurationKeys.USERNAME] == "email2@test.com" + assert config_entry.data[RenaultConfigurationKeys.PASSWORD] == "test2" + assert config_entry.data[RenaultConfigurationKeys.LOGIN_TOKEN] == MOCK_LOGIN_TOKEN + assert ( + config_entry.data[RenaultConfigurationKeys.KAMEREON_ACCOUNT_ID] + == "account_id_1" + ) + assert config_entry.data[RenaultConfigurationKeys.LOCALE] == "fr_FR" assert len(mock_setup_entry.mock_calls) == 1 @@ -395,9 +414,18 @@ async def test_reconfigure_mismatch( assert not result["errors"] data_schema = result["data_schema"].schema - assert get_schema_suggested_value(data_schema, CONF_LOCALE) == "fr_FR" - assert get_schema_suggested_value(data_schema, CONF_USERNAME) == "email@test.com" - assert get_schema_suggested_value(data_schema, CONF_PASSWORD) == "test" + assert ( + get_schema_suggested_value(data_schema, RenaultConfigurationKeys.LOCALE) + == "fr_FR" + ) + assert ( + get_schema_suggested_value(data_schema, RenaultConfigurationKeys.USERNAME) + == "email@test.com" + ) + assert ( + get_schema_suggested_value(data_schema, RenaultConfigurationKeys.PASSWORD) + == "test" + ) renault_account = AsyncMock() type(renault_account).account_id = PropertyMock(return_value="account_id_other") @@ -422,9 +450,9 @@ async def test_reconfigure_mismatch( result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ - CONF_LOCALE: "fr_FR", - CONF_USERNAME: "email2@test.com", - CONF_PASSWORD: "test2", + RenaultConfigurationKeys.LOCALE: "fr_FR", + RenaultConfigurationKeys.USERNAME: "email2@test.com", + RenaultConfigurationKeys.PASSWORD: "test2", }, ) @@ -432,10 +460,13 @@ async def test_reconfigure_mismatch( assert result["reason"] == "unique_id_mismatch" # Unchanged values - assert config_entry.data[CONF_USERNAME] == "email@test.com" - assert config_entry.data[CONF_PASSWORD] == "test" - assert config_entry.data[CONF_LOGIN_TOKEN] == MOCK_LOGIN_TOKEN - assert config_entry.data[CONF_KAMEREON_ACCOUNT_ID] == "account_id_1" - assert config_entry.data[CONF_LOCALE] == "fr_FR" + assert config_entry.data[RenaultConfigurationKeys.USERNAME] == "email@test.com" + assert config_entry.data[RenaultConfigurationKeys.PASSWORD] == "test" + assert config_entry.data[RenaultConfigurationKeys.LOGIN_TOKEN] == MOCK_LOGIN_TOKEN + assert ( + config_entry.data[RenaultConfigurationKeys.KAMEREON_ACCOUNT_ID] + == "account_id_1" + ) + assert config_entry.data[RenaultConfigurationKeys.LOCALE] == "fr_FR" assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/renault/test_init.py b/tests/components/renault/test_init.py index e37416cf118e..e30286ebb1e8 100644 --- a/tests/components/renault/test_init.py +++ b/tests/components/renault/test_init.py @@ -10,19 +10,13 @@ from renault_api.gigya.exceptions import GigyaException, InvalidCredentialsExcep from renault_api.renault_session import RenaultSession from syrupy.assertion import SnapshotAssertion -from homeassistant.components.renault.const import ( - CONF_KAMEREON_ACCOUNT_ID, - CONF_LOCALE, - CONF_LOGIN_TOKEN, - DOMAIN, -) +from homeassistant.components.renault.const import DOMAIN, RenaultConfigurationKeys from homeassistant.config_entries import ( SOURCE_REAUTH, SOURCE_USER, ConfigEntry, ConfigEntryState, ) -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component @@ -34,10 +28,10 @@ from tests.typing import WebSocketGenerator # Config data of an entry created before the login token was stored. MOCK_CONFIG_NO_TOKEN = { - CONF_USERNAME: "email@test.com", - CONF_PASSWORD: "test", - CONF_KAMEREON_ACCOUNT_ID: MOCK_ACCOUNT_ID, - CONF_LOCALE: "fr_FR", + RenaultConfigurationKeys.USERNAME: "email@test.com", + RenaultConfigurationKeys.PASSWORD: "test", + RenaultConfigurationKeys.KAMEREON_ACCOUNT_ID: MOCK_ACCOUNT_ID, + RenaultConfigurationKeys.LOCALE: "fr_FR", } @@ -102,7 +96,10 @@ async def test_setup_entry_password_login( assert mock_login.called assert legacy_config_entry.state is ConfigEntryState.LOADED # The obtained login token is persisted so future setups skip the password. - assert legacy_config_entry.data[CONF_LOGIN_TOKEN] == MOCK_LOGIN_TOKEN + assert ( + legacy_config_entry.data[RenaultConfigurationKeys.LOGIN_TOKEN] + == MOCK_LOGIN_TOKEN + ) async def test_setup_entry_bad_password( diff --git a/tests/components/renault/test_services.py b/tests/components/renault/test_services.py index f2a453cbca9e..68cb6b57084e 100644 --- a/tests/components/renault/test_services.py +++ b/tests/components/renault/test_services.py @@ -2,6 +2,7 @@ from collections.abc import Generator from datetime import datetime +from enum import StrEnum from unittest.mock import patch import pytest @@ -11,13 +12,8 @@ from renault_api.kamereon.models import ChargeSchedule, HvacSchedule from syrupy.assertion import SnapshotAssertion from homeassistant.components.renault.const import DOMAIN -from homeassistant.components.renault.services import ( - ATTR_SCHEDULES, - ATTR_VEHICLE, - ATTR_WHEN, -) +from homeassistant.components.renault.services import RenaultServiceArgument from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_TEMPERATURE from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import device_registry as dr @@ -27,6 +23,16 @@ from tests.common import async_load_fixture pytestmark = pytest.mark.usefixtures("patch_renault_account", "patch_get_vehicles") +class RenaultService(StrEnum): + """Renault service names.""" + + AC_CANCEL = "ac_cancel" + AC_SET_SCHEDULES = "ac_set_schedules" + AC_START = "ac_start" + CHARGE_SET_SCHEDULES = "charge_set_schedules" + CHARGE_START = "charge_start" + + @pytest.fixture(autouse=True) def override_platforms() -> Generator[None]: """Override PLATFORMS.""" @@ -56,7 +62,7 @@ async def test_service_set_ac_cancel( await hass.async_block_till_done() data = { - ATTR_VEHICLE: get_device_id(hass), + RenaultServiceArgument.VEHICLE: get_device_id(hass), } with patch( @@ -68,7 +74,7 @@ async def test_service_set_ac_cancel( ), ) as mock_action: await hass.services.async_call( - DOMAIN, "ac_cancel", service_data=data, blocking=True + DOMAIN, RenaultService.AC_CANCEL, service_data=data, blocking=True ) assert len(mock_action.mock_calls) == 1 assert mock_action.mock_calls[0][1] == () @@ -83,8 +89,8 @@ async def test_service_set_ac_start_simple( temperature = 13.5 data = { - ATTR_VEHICLE: get_device_id(hass), - ATTR_TEMPERATURE: temperature, + RenaultServiceArgument.VEHICLE: get_device_id(hass), + RenaultServiceArgument.TEMPERATURE: temperature, } with patch( @@ -96,7 +102,7 @@ async def test_service_set_ac_start_simple( ), ) as mock_action: await hass.services.async_call( - DOMAIN, "ac_start", service_data=data, blocking=True + DOMAIN, RenaultService.AC_START, service_data=data, blocking=True ) assert len(mock_action.mock_calls) == 1 assert mock_action.mock_calls[0][1] == (temperature, None) @@ -112,9 +118,9 @@ async def test_service_set_ac_start_with_date( temperature = 13.5 when = datetime(2025, 8, 23, 17, 12, 45) data = { - ATTR_VEHICLE: get_device_id(hass), - ATTR_TEMPERATURE: temperature, - ATTR_WHEN: when, + RenaultServiceArgument.VEHICLE: get_device_id(hass), + RenaultServiceArgument.TEMPERATURE: temperature, + RenaultServiceArgument.WHEN: when, } with patch( @@ -126,7 +132,7 @@ async def test_service_set_ac_start_with_date( ), ) as mock_action: await hass.services.async_call( - DOMAIN, "ac_start", service_data=data, blocking=True + DOMAIN, RenaultService.AC_START, service_data=data, blocking=True ) assert len(mock_action.mock_calls) == 1 assert mock_action.mock_calls[0][1] == (temperature, when) @@ -140,7 +146,7 @@ async def test_service_charge_start_simple( await hass.async_block_till_done() data = { - ATTR_VEHICLE: get_device_id(hass), + RenaultServiceArgument.VEHICLE: get_device_id(hass), } with patch( @@ -152,7 +158,7 @@ async def test_service_charge_start_simple( ), ) as mock_action: await hass.services.async_call( - DOMAIN, "charge_start", service_data=data, blocking=True + DOMAIN, RenaultService.CHARGE_START, service_data=data, blocking=True ) assert len(mock_action.mock_calls) == 1 assert mock_action.mock_calls[0][1] == (None,) @@ -167,8 +173,8 @@ async def test_service_charge_start_with_date( when = datetime(2025, 8, 23, 17, 12, 45) data = { - ATTR_VEHICLE: get_device_id(hass), - ATTR_WHEN: when, + RenaultServiceArgument.VEHICLE: get_device_id(hass), + RenaultServiceArgument.WHEN: when, } with patch( @@ -180,7 +186,7 @@ async def test_service_charge_start_with_date( ), ) as mock_action: await hass.services.async_call( - DOMAIN, "charge_start", service_data=data, blocking=True + DOMAIN, RenaultService.CHARGE_START, service_data=data, blocking=True ) assert len(mock_action.mock_calls) == 1 assert mock_action.mock_calls[0][1] == (when,) @@ -195,8 +201,8 @@ async def test_service_set_charge_schedule( schedules = {"id": 2} data = { - ATTR_VEHICLE: get_device_id(hass), - ATTR_SCHEDULES: schedules, + RenaultServiceArgument.VEHICLE: get_device_id(hass), + RenaultServiceArgument.SCHEDULES: schedules, } with ( @@ -219,7 +225,10 @@ async def test_service_set_charge_schedule( ) as mock_action, ): await hass.services.async_call( - DOMAIN, "charge_set_schedules", service_data=data, blocking=True + DOMAIN, + RenaultService.CHARGE_SET_SCHEDULES, + service_data=data, + blocking=True, ) assert len(mock_action.mock_calls) == 1 mock_call_data: list[ChargeSchedule] = mock_action.mock_calls[0][1][0] @@ -247,8 +256,8 @@ async def test_service_set_charge_schedule_multi( {"id": 3}, ] data = { - ATTR_VEHICLE: get_device_id(hass), - ATTR_SCHEDULES: schedules, + RenaultServiceArgument.VEHICLE: get_device_id(hass), + RenaultServiceArgument.SCHEDULES: schedules, } with ( @@ -271,7 +280,10 @@ async def test_service_set_charge_schedule_multi( ) as mock_action, ): await hass.services.async_call( - DOMAIN, "charge_set_schedules", service_data=data, blocking=True + DOMAIN, + RenaultService.CHARGE_SET_SCHEDULES, + service_data=data, + blocking=True, ) assert len(mock_action.mock_calls) == 1 mock_call_data: list[ChargeSchedule] = mock_action.mock_calls[0][1][0] @@ -296,8 +308,8 @@ async def test_service_set_ac_schedule( schedules = {"id": 2} data = { - ATTR_VEHICLE: get_device_id(hass), - ATTR_SCHEDULES: schedules, + RenaultServiceArgument.VEHICLE: get_device_id(hass), + RenaultServiceArgument.SCHEDULES: schedules, } with ( @@ -319,7 +331,7 @@ async def test_service_set_ac_schedule( ) as mock_action, ): await hass.services.async_call( - DOMAIN, "ac_set_schedules", service_data=data, blocking=True + DOMAIN, RenaultService.AC_SET_SCHEDULES, service_data=data, blocking=True ) assert len(mock_action.mock_calls) == 1 mock_call_data: list[ChargeSchedule] = mock_action.mock_calls[0][1][0] @@ -347,8 +359,8 @@ async def test_service_set_ac_schedule_multi( {"id": 4}, ] data = { - ATTR_VEHICLE: get_device_id(hass), - ATTR_SCHEDULES: schedules, + RenaultServiceArgument.VEHICLE: get_device_id(hass), + RenaultServiceArgument.SCHEDULES: schedules, } with ( @@ -370,7 +382,7 @@ async def test_service_set_ac_schedule_multi( ) as mock_action, ): await hass.services.async_call( - DOMAIN, "ac_set_schedules", service_data=data, blocking=True + DOMAIN, RenaultService.AC_SET_SCHEDULES, service_data=data, blocking=True ) assert len(mock_action.mock_calls) == 1 mock_call_data: list[HvacSchedule] = mock_action.mock_calls[0][1][0] @@ -393,11 +405,11 @@ async def test_service_invalid_device_id( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - data = {ATTR_VEHICLE: "some_random_id"} + data = {RenaultServiceArgument.VEHICLE: "some_random_id"} with pytest.raises(ServiceValidationError) as err: await hass.services.async_call( - DOMAIN, "ac_cancel", service_data=data, blocking=True + DOMAIN, RenaultService.AC_CANCEL, service_data=data, blocking=True ) assert err.value.translation_key == "invalid_device_id" assert err.value.translation_placeholders == {"device_id": "some_random_id"} @@ -421,11 +433,11 @@ async def test_service_invalid_device_id2( identifiers={(DOMAIN, "VF1AAAAA111222333")}, ).id - data = {ATTR_VEHICLE: device_id} + data = {RenaultServiceArgument.VEHICLE: device_id} with pytest.raises(ServiceValidationError) as err: await hass.services.async_call( - DOMAIN, "ac_cancel", service_data=data, blocking=True + DOMAIN, RenaultService.AC_CANCEL, service_data=data, blocking=True ) assert err.value.translation_key == "no_config_entry_for_device" assert err.value.translation_placeholders == {"device_id": "REG-NUMBER"} @@ -439,7 +451,7 @@ async def test_service_exception( await hass.async_block_till_done() data = { - ATTR_VEHICLE: get_device_id(hass), + RenaultServiceArgument.VEHICLE: get_device_id(hass), } with ( @@ -450,7 +462,7 @@ async def test_service_exception( pytest.raises(HomeAssistantError, match="Didn't work"), ): await hass.services.async_call( - DOMAIN, "ac_cancel", service_data=data, blocking=True + DOMAIN, RenaultService.AC_CANCEL, service_data=data, blocking=True ) assert len(mock_action.mock_calls) == 1 assert mock_action.mock_calls[0][1] == ()