mirror of
https://github.com/home-assistant/core.git
synced 2026-05-29 12:14:26 +02:00
Compare commits
14 Commits
frenck-2026-0637
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d3bb346e9 | |||
| d13721980e | |||
| ac6b5a5850 | |||
| 16dfa99673 | |||
| f51a02bbda | |||
| 6a51b21242 | |||
| 5eb502851c | |||
| ef20418c76 | |||
| 94ca34fd0c | |||
| 8634c22a53 | |||
| 5681ba40f1 | |||
| 8a9a1c5fed | |||
| c587e101af | |||
| 6eeeac46f3 |
@@ -344,13 +344,13 @@ jobs:
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: matrix.registry == 'docker.io/homeassistant'
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -523,7 +523,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
|
||||
@@ -28,11 +28,11 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"name": "Sony Bravia TV",
|
||||
"codeowners": ["@bieniu", "@Drafteed"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["ssdp"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/braviatv",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
{
|
||||
"entity": {
|
||||
"light": {
|
||||
"hue_grouped_light": {
|
||||
"default": "mdi:lightbulb-group",
|
||||
"state": {
|
||||
"off": "mdi:lightbulb-group-off"
|
||||
}
|
||||
},
|
||||
"hue_light": {
|
||||
"state_attributes": {
|
||||
"effect": {
|
||||
|
||||
@@ -85,7 +85,6 @@ class GroupedHueLight(HueBaseEntity, LightEntity):
|
||||
|
||||
entity_description = LightEntityDescription(
|
||||
key="hue_grouped_light",
|
||||
icon="mdi:lightbulb-group",
|
||||
has_entity_name=True,
|
||||
name=None,
|
||||
)
|
||||
|
||||
@@ -73,20 +73,26 @@ async def _get_endpoint_id(
|
||||
device_reg = dr.async_get(call.hass)
|
||||
device_id = call.data[ATTR_DEVICE_ID]
|
||||
device = device_reg.async_get(device_id)
|
||||
assert device
|
||||
|
||||
if device is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_target",
|
||||
)
|
||||
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
endpoint_data = None
|
||||
for data in coordinator.data.values():
|
||||
if (
|
||||
DOMAIN,
|
||||
f"{config_entry.entry_id}_{data.endpoint.id}",
|
||||
) in device.identifiers:
|
||||
endpoint_data = data
|
||||
break
|
||||
return data.endpoint.id
|
||||
|
||||
assert endpoint_data
|
||||
return endpoint_data.endpoint.id
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_target",
|
||||
)
|
||||
|
||||
|
||||
async def _get_container_and_endpoint_ids(
|
||||
@@ -95,6 +101,7 @@ async def _get_container_and_endpoint_ids(
|
||||
"""Get config entry, endpoint ID and container ID from the container device ID."""
|
||||
device_reg = dr.async_get(call.hass)
|
||||
device = device_reg.async_get(call.data[ATTR_CONTAINER_DEVICE_ID])
|
||||
|
||||
if device is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -29,7 +29,11 @@ class IRobotEntity(Entity):
|
||||
model=self.vacuum_state.get("sku"),
|
||||
name=str(self.vacuum_state.get("name")),
|
||||
sw_version=self.vacuum_state.get("softwareVer"),
|
||||
hw_version=self.vacuum_state.get("hardwareRev"),
|
||||
hw_version=(
|
||||
str(hw_rev)
|
||||
if (hw_rev := self.vacuum_state.get("hardwareRev")) is not None
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
if mac_address := self.vacuum_state.get("hwPartsRev", {}).get(
|
||||
|
||||
@@ -132,6 +132,9 @@ def async_manage_outbound_websocket_incorrectly_enabled_issue(
|
||||
|
||||
device = entry.runtime_data.rpc.device
|
||||
|
||||
if not device.initialized:
|
||||
return
|
||||
|
||||
if (
|
||||
(ws_config := device.config.get("ws"))
|
||||
and ws_config["enable"]
|
||||
@@ -169,6 +172,9 @@ def async_manage_open_wifi_ap_issue(
|
||||
|
||||
device = entry.runtime_data.rpc.device
|
||||
|
||||
if not device.initialized:
|
||||
return
|
||||
|
||||
# Check if WiFi AP is enabled and is open (no password)
|
||||
if (
|
||||
(wifi_config := device.config.get("wifi"))
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from homeassistant.components.device_tracker import TrackerEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import STATE_HOME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
@@ -83,11 +82,3 @@ class TeslaFleetDeviceTrackerRouteEntity(TeslaFleetDeviceTrackerEntity):
|
||||
self.get("drive_state_active_route_longitude", False) is None
|
||||
or self.get("drive_state_active_route_latitude", False) is None
|
||||
)
|
||||
|
||||
@property
|
||||
def location_name(self) -> str | None:
|
||||
"""Return a location name for the current location of the device."""
|
||||
location = self.get("drive_state_active_route_destination")
|
||||
if location == "Home":
|
||||
return STATE_HOME
|
||||
return location
|
||||
|
||||
@@ -280,6 +280,10 @@ VEHICLE_DESCRIPTIONS: tuple[TeslaFleetSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfLength.MILES,
|
||||
device_class=SensorDeviceClass.DISTANCE,
|
||||
),
|
||||
TeslaFleetSensorEntityDescription(
|
||||
key="drive_state_active_route_destination",
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ from homeassistant.components.device_tracker import (
|
||||
TrackerEntity,
|
||||
TrackerEntityDescription,
|
||||
)
|
||||
from homeassistant.const import STATE_HOME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
@@ -31,12 +30,6 @@ class TeslemetryDeviceTrackerEntityDescription(TrackerEntityDescription):
|
||||
[TeslemetryStreamVehicle, Callable[[TeslaLocation | None], None]],
|
||||
Callable[[], None],
|
||||
]
|
||||
name_listener: (
|
||||
Callable[
|
||||
[TeslemetryStreamVehicle, Callable[[str | None], None]], Callable[[], None]
|
||||
]
|
||||
| None
|
||||
) = None
|
||||
streaming_firmware: str
|
||||
polling_prefix: str | None = None
|
||||
|
||||
@@ -54,9 +47,6 @@ DESCRIPTIONS: tuple[TeslemetryDeviceTrackerEntityDescription, ...] = (
|
||||
value_listener=lambda vehicle, callback: vehicle.listen_DestinationLocation(
|
||||
callback
|
||||
),
|
||||
name_listener=lambda vehicle, callback: vehicle.listen_DestinationName(
|
||||
callback
|
||||
),
|
||||
streaming_firmware="2024.26",
|
||||
),
|
||||
TeslemetryDeviceTrackerEntityDescription(
|
||||
@@ -126,11 +116,6 @@ class TeslemetryVehiclePollingDeviceTrackerEntity(
|
||||
self._attr_longitude = self.get(
|
||||
f"{self.entity_description.polling_prefix}_longitude"
|
||||
)
|
||||
self._attr_location_name = self.get(
|
||||
f"{self.entity_description.polling_prefix}_destination"
|
||||
)
|
||||
if self._attr_location_name == "Home":
|
||||
self._attr_location_name = STATE_HOME
|
||||
self._attr_available = (
|
||||
self._attr_latitude is not None and self._attr_longitude is not None
|
||||
)
|
||||
@@ -158,28 +143,14 @@ class TeslemetryStreamingDeviceTrackerEntity(
|
||||
if (state := await self.async_get_last_state()) is not None:
|
||||
self._attr_latitude = state.attributes.get("latitude")
|
||||
self._attr_longitude = state.attributes.get("longitude")
|
||||
self._attr_location_name = state.attributes.get("location_name")
|
||||
self.async_on_remove(
|
||||
self.entity_description.value_listener(
|
||||
self.vehicle.stream_vehicle, self._location_callback
|
||||
)
|
||||
)
|
||||
if self.entity_description.name_listener:
|
||||
self.async_on_remove(
|
||||
self.entity_description.name_listener(
|
||||
self.vehicle.stream_vehicle, self._name_callback
|
||||
)
|
||||
)
|
||||
|
||||
def _location_callback(self, location: TeslaLocation | None) -> None:
|
||||
"""Update the value of the entity."""
|
||||
self._attr_latitude = None if location is None else location.latitude
|
||||
self._attr_longitude = None if location is None else location.longitude
|
||||
self.async_write_ha_state()
|
||||
|
||||
def _name_callback(self, name: str | None) -> None:
|
||||
"""Update the value of the entity."""
|
||||
self._attr_location_name = name
|
||||
if self._attr_location_name == "Home":
|
||||
self._attr_location_name = STATE_HOME
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -510,6 +510,14 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = (
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
TeslemetryVehicleSensorEntityDescription(
|
||||
key="drive_state_active_route_destination",
|
||||
polling=True,
|
||||
streaming_listener=lambda vehicle, callback: vehicle.listen_DestinationName(
|
||||
callback
|
||||
),
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
TeslemetryVehicleSensorEntityDescription(
|
||||
key="drive_state_active_route_traffic_minutes_delay",
|
||||
polling=True,
|
||||
|
||||
@@ -75,6 +75,10 @@ class VolvoLock(VolvoEntity, LockEntity):
|
||||
|
||||
def _update_state(self, api_field: VolvoCarsApiBaseModel | None) -> None:
|
||||
"""Update the state of the entity."""
|
||||
if api_field is None:
|
||||
self._attr_is_locked = None
|
||||
return
|
||||
|
||||
assert isinstance(api_field, VolvoCarsValue)
|
||||
self._attr_is_locked = api_field.value == "LOCKED"
|
||||
|
||||
|
||||
@@ -468,7 +468,7 @@ class WyomingAssistSatellite(WyomingSatelliteEntity, AssistSatelliteEntity):
|
||||
|
||||
async def on_restart(self) -> None:
|
||||
"""Block until pipeline loop will be restarted."""
|
||||
_LOGGER.warning(
|
||||
_LOGGER.debug(
|
||||
"Satellite has been disconnected. Reconnecting in %s second(s)",
|
||||
_RECONNECT_SECONDS,
|
||||
)
|
||||
|
||||
@@ -6,8 +6,6 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
LocalOAuth2ImplementationWithPkce,
|
||||
)
|
||||
|
||||
from .const import YOTO_AUDIENCE, YOTO_SCOPES
|
||||
|
||||
AUTHORIZE_URL = "https://login.yotoplay.com/authorize"
|
||||
TOKEN_URL = "https://login.yotoplay.com/oauth/token"
|
||||
|
||||
@@ -16,9 +14,9 @@ async def async_get_auth_implementation(
|
||||
hass: HomeAssistant,
|
||||
auth_domain: str,
|
||||
credential: ClientCredential,
|
||||
) -> YotoOAuth2Implementation:
|
||||
"""Return a Yoto OAuth2 implementation backed by the user's credential."""
|
||||
return YotoOAuth2Implementation(
|
||||
) -> LocalOAuth2ImplementationWithPkce:
|
||||
"""Return a Yoto OAuth2 implementation with PKCE."""
|
||||
return LocalOAuth2ImplementationWithPkce(
|
||||
hass,
|
||||
auth_domain,
|
||||
credential.client_id,
|
||||
@@ -26,15 +24,3 @@ async def async_get_auth_implementation(
|
||||
TOKEN_URL,
|
||||
credential.client_secret,
|
||||
)
|
||||
|
||||
|
||||
class YotoOAuth2Implementation(LocalOAuth2ImplementationWithPkce):
|
||||
"""Yoto OAuth2 implementation with PKCE, audience and scopes."""
|
||||
|
||||
@property
|
||||
def extra_authorize_data(self) -> dict:
|
||||
"""Append Yoto's audience and scopes to every authorize URL."""
|
||||
return super().extra_authorize_data | {
|
||||
"audience": YOTO_AUDIENCE,
|
||||
"scope": " ".join(YOTO_SCOPES),
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ from yoto_api import YotoError, get_account_id
|
||||
from homeassistant.config_entries import ConfigFlowResult
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
|
||||
from .const import _LOGGER, DOMAIN
|
||||
from .const import _LOGGER, DOMAIN, YOTO_AUDIENCE, YOTO_SCOPES
|
||||
|
||||
|
||||
class YotoOAuth2FlowHandler(
|
||||
@@ -23,6 +23,14 @@ class YotoOAuth2FlowHandler(
|
||||
"""Return the logger used for the OAuth2 flow."""
|
||||
return _LOGGER
|
||||
|
||||
@property
|
||||
def extra_authorize_data(self) -> dict[str, Any]:
|
||||
"""Append Yoto's audience and scopes to the authorize URL."""
|
||||
return {
|
||||
"audience": YOTO_AUDIENCE,
|
||||
"scope": " ".join(YOTO_SCOPES),
|
||||
}
|
||||
|
||||
async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Identify the Yoto account from the access token."""
|
||||
try:
|
||||
|
||||
@@ -8,7 +8,7 @@ import zeversolar
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
@@ -35,4 +35,7 @@ class ZeversolarCoordinator(DataUpdateCoordinator[zeversolar.ZeverSolarData]):
|
||||
|
||||
async def _async_update_data(self) -> zeversolar.ZeverSolarData:
|
||||
"""Fetch the latest data from the source."""
|
||||
return await self.hass.async_add_executor_job(self._client.get_data)
|
||||
try:
|
||||
return await self.hass.async_add_executor_job(self._client.get_data)
|
||||
except zeversolar.ZeverSolarError as err:
|
||||
raise UpdateFailed(err) from err
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Test for Portainer services."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from pyportainer import (
|
||||
PortainerAuthenticationError,
|
||||
@@ -20,6 +20,7 @@ from homeassistant.components.portainer.services import (
|
||||
ATTR_TIMEOUT,
|
||||
SERVICE_PRUNE_IMAGES,
|
||||
SERVICE_RECREATE_CONTAINER,
|
||||
_get_endpoint_id,
|
||||
)
|
||||
from homeassistant.const import ATTR_DEVICE_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -281,6 +282,34 @@ async def test_service_validation_errors(
|
||||
)
|
||||
mock_portainer_client.container_recreate.assert_not_called()
|
||||
|
||||
with pytest.raises(ServiceValidationError, match="Invalid device targeted"):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_PRUNE_IMAGES,
|
||||
{ATTR_DEVICE_ID: container.id},
|
||||
blocking=True,
|
||||
)
|
||||
mock_portainer_client.images_prune.assert_not_called()
|
||||
|
||||
|
||||
async def test_service_prune_images_device_gone(
|
||||
hass: HomeAssistant,
|
||||
mock_portainer_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test _get_endpoint_id raises when the device ID no longer exists in the registry."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
loaded_entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id)
|
||||
assert loaded_entry is not None
|
||||
|
||||
mock_call = MagicMock()
|
||||
mock_call.hass = hass
|
||||
mock_call.data = {ATTR_DEVICE_ID: "nonexistent_device_id"}
|
||||
|
||||
with pytest.raises(ServiceValidationError):
|
||||
await _get_endpoint_id(mock_call, loaded_entry)
|
||||
mock_portainer_client.images_prune.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "message"),
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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] == ()
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import Any
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from aioshelly.const import MODEL_PLUG, MODEL_WALL_DISPLAY
|
||||
from aioshelly.exceptions import DeviceConnectionError, RpcCallError
|
||||
from aioshelly.exceptions import DeviceConnectionError, NotInitialized, RpcCallError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.shelly.const import (
|
||||
@@ -179,6 +179,22 @@ async def test_outbound_websocket_incorrectly_enabled_issue(
|
||||
assert len(issue_registry.issues) == 0
|
||||
|
||||
|
||||
async def test_repairs_skipped_when_device_not_initialized(
|
||||
hass: HomeAssistant,
|
||||
mock_rpc_device: Mock,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
) -> None:
|
||||
"""Test repair checks are skipped when the RPC device is not initialized."""
|
||||
mock_rpc_device.initialized = False
|
||||
type(mock_rpc_device).config = property(
|
||||
lambda self: (_ for _ in ()).throw(NotInitialized)
|
||||
)
|
||||
|
||||
await init_integration(hass, 2)
|
||||
|
||||
assert len(issue_registry.issues) == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exception", [DeviceConnectionError, RpcCallError(999, "Unknown error")]
|
||||
)
|
||||
|
||||
@@ -108,6 +108,6 @@
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'home',
|
||||
'state': 'not_home',
|
||||
})
|
||||
# ---
|
||||
|
||||
@@ -3216,6 +3216,69 @@
|
||||
'state': 'stopped',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.test_destination-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.test_destination',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Destination',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Destination',
|
||||
'platform': 'tesla_fleet',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'drive_state_active_route_destination',
|
||||
'unique_id': 'LRWXF7EK4KC700000-drive_state_active_route_destination',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.test_destination-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Test Destination',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_destination',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'Home',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.test_destination-statealt]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Test Destination',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_destination',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.test_distance_to_arrival-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
|
||||
@@ -108,7 +108,7 @@
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'home',
|
||||
'state': 'not_home',
|
||||
})
|
||||
# ---
|
||||
# name: test_device_tracker_alt[device_tracker.test_location-statealt]
|
||||
|
||||
@@ -3228,6 +3228,69 @@
|
||||
'state': 'stopped',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.test_destination-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.test_destination',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Destination',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Destination',
|
||||
'platform': 'teslemetry',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'drive_state_active_route_destination',
|
||||
'unique_id': 'LRW3F7EK4NC700000-drive_state_active_route_destination',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.test_destination-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Test Destination',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_destination',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'Home',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.test_destination-statealt]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Test Destination',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.test_destination',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unavailable',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.test_distance_to_arrival-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
|
||||
@@ -81,7 +81,6 @@ async def test_device_tracker_streaming(
|
||||
"latitude": 3.0,
|
||||
"longitude": 4.0,
|
||||
},
|
||||
Signal.DESTINATION_NAME: "Home",
|
||||
Signal.ORIGIN_LOCATION: None,
|
||||
},
|
||||
"createdAt": "2024-10-04T10:45:17.537Z",
|
||||
@@ -91,7 +90,7 @@ async def test_device_tracker_streaming(
|
||||
|
||||
# Assert the entities have correct state values
|
||||
assert hass.states.get("device_tracker.test_location").state == "not_home"
|
||||
assert hass.states.get("device_tracker.test_route").state == "home"
|
||||
assert hass.states.get("device_tracker.test_route").state == "not_home"
|
||||
assert hass.states.get("device_tracker.test_origin").state == "unknown"
|
||||
|
||||
# Reload the entry
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"""Test Volvo locks."""
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from datetime import timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
from volvocarsapi.api import VolvoCarsApi
|
||||
@@ -14,7 +16,8 @@ from homeassistant.components.lock import (
|
||||
SERVICE_UNLOCK,
|
||||
LockState,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||
from homeassistant.components.volvo.coordinator import FAST_INTERVAL
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
@@ -22,7 +25,7 @@ from homeassistant.helpers import entity_registry as er
|
||||
from . import configure_mock
|
||||
from .const import DEFAULT_VIN
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_api", "full_model")
|
||||
@@ -134,3 +137,28 @@ async def test_unlock_failure(
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get(entity_id).state == LockState.LOCKED
|
||||
|
||||
|
||||
@pytest.mark.freeze_time("2025-05-31T10:00:00+00:00")
|
||||
@pytest.mark.usefixtures("full_model")
|
||||
async def test_lock_unavailable_when_api_field_missing(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
setup_integration: Callable[[], Awaitable[bool]],
|
||||
mock_api: VolvoCarsApi,
|
||||
) -> None:
|
||||
"""Test lock becomes unavailable when centralLock is missing from API response."""
|
||||
|
||||
with patch("homeassistant.components.volvo.PLATFORMS", [Platform.LOCK]):
|
||||
assert await setup_integration()
|
||||
|
||||
entity_id = "lock.volvo_xc40_lock"
|
||||
assert hass.states.get(entity_id).state == LockState.LOCKED
|
||||
|
||||
# Simulate API returning doors data without centralLock
|
||||
configure_mock(mock_api.async_get_doors_status, return_value={})
|
||||
freezer.tick(timedelta(minutes=FAST_INTERVAL))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
"""Test the sensor classes."""
|
||||
|
||||
from unittest.mock import patch
|
||||
from datetime import timedelta
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
from zeversolar.exceptions import ZeverSolarError
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.const import STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
|
||||
|
||||
async def test_sensors(
|
||||
@@ -25,3 +28,23 @@ async def test_sensors(
|
||||
await snapshot_platform(
|
||||
hass, entity_registry, snapshot, init_integration.entry_id
|
||||
)
|
||||
|
||||
|
||||
async def test_sensor_update_failed(
|
||||
hass: HomeAssistant,
|
||||
init_integration: MockConfigEntry,
|
||||
mock_zeversolar_client: MagicMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test entities become unavailable after a failed coordinator update."""
|
||||
assert hass.states.get("sensor.zeversolar_sensor_energy_today").state is not None
|
||||
|
||||
mock_zeversolar_client.get_data.side_effect = ZeverSolarError
|
||||
freezer.tick(timedelta(minutes=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (
|
||||
hass.states.get("sensor.zeversolar_sensor_energy_today").state
|
||||
== STATE_UNAVAILABLE
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user