mirror of
https://github.com/home-assistant/core.git
synced 2026-05-21 16:25:18 +02:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d294b04b79 | |||
| 8b0e9060b3 | |||
| 39066b6e3a | |||
| a23a9b350b | |||
| fdaa807ca8 | |||
| f290dcc03f | |||
| 654408cc76 | |||
| 1f814faad8 | |||
| 6e00eecfcd |
Generated
+2
-2
@@ -1413,8 +1413,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/pushover/ @engrbm87
|
||||
/homeassistant/components/pvoutput/ @frenck
|
||||
/tests/components/pvoutput/ @frenck
|
||||
/homeassistant/components/pvpc_hourly_pricing/ @azogue
|
||||
/tests/components/pvpc_hourly_pricing/ @azogue
|
||||
/homeassistant/components/pvpc_hourly_pricing/ @azogue @chiro79
|
||||
/tests/components/pvpc_hourly_pricing/ @azogue @chiro79
|
||||
/homeassistant/components/pyload/ @tr4nt0r
|
||||
/tests/components/pyload/ @tr4nt0r
|
||||
/homeassistant/components/qbittorrent/ @geoffreylagaisse @finder39
|
||||
|
||||
@@ -173,7 +173,6 @@ class BlinkConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "unknown"
|
||||
|
||||
config_entry = self._get_reconfigure_entry()
|
||||
# pylint: disable-next=home-assistant-config-flow-field-not-translated
|
||||
return self.async_show_form(
|
||||
step_id="reconfigure",
|
||||
data_schema=vol.Schema(
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
"""Provide functionality to keep track of devices."""
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME # noqa: F401
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .config_entry import ( # noqa: F401
|
||||
DATA_COMPONENT,
|
||||
BaseScannerEntity,
|
||||
BaseTrackerEntity,
|
||||
ScannerEntity,
|
||||
ScannerEntityDescription,
|
||||
TrackerEntity,
|
||||
@@ -33,6 +40,8 @@ from .const import ( # noqa: F401
|
||||
DEFAULT_TRACK_NEW,
|
||||
DOMAIN,
|
||||
ENTITY_ID_FORMAT,
|
||||
LOGGER,
|
||||
PLATFORM_TYPE_LEGACY,
|
||||
SCAN_INTERVAL,
|
||||
SourceType,
|
||||
)
|
||||
@@ -44,7 +53,9 @@ from .legacy import ( # noqa: F401
|
||||
SOURCE_TYPES,
|
||||
AsyncSeeCallback,
|
||||
DeviceScanner,
|
||||
DeviceTracker,
|
||||
SeeCallback,
|
||||
async_create_platform_type,
|
||||
async_setup_integration as async_setup_legacy_integration,
|
||||
see,
|
||||
)
|
||||
@@ -57,5 +68,43 @@ def is_on(hass: HomeAssistant, entity_id: str) -> bool:
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the device tracker."""
|
||||
async_setup_legacy_integration(hass, config)
|
||||
component = hass.data[DATA_COMPONENT] = EntityComponent[BaseTrackerEntity](
|
||||
LOGGER, DOMAIN, hass, SCAN_INTERVAL
|
||||
)
|
||||
component.config = {}
|
||||
component.register_shutdown()
|
||||
|
||||
# The tracker is loaded in the async_setup_legacy_integration task so
|
||||
# we create a future to avoid waiting on it here so that only
|
||||
# async_platform_discovered will have to wait in the rare event
|
||||
# a custom component still uses the legacy device tracker discovery.
|
||||
tracker_future: asyncio.Future[DeviceTracker] = hass.loop.create_future()
|
||||
|
||||
async def async_platform_discovered(
|
||||
p_type: str, info: dict[str, Any] | None
|
||||
) -> None:
|
||||
"""Load a platform."""
|
||||
platform = await async_create_platform_type(hass, config, p_type, {})
|
||||
|
||||
if platform is None:
|
||||
return
|
||||
|
||||
if platform.type != PLATFORM_TYPE_LEGACY:
|
||||
await component.async_setup_platform(p_type, {}, info)
|
||||
return
|
||||
|
||||
tracker = await tracker_future
|
||||
await platform.async_setup_legacy(hass, tracker, info)
|
||||
|
||||
discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered)
|
||||
#
|
||||
# Legacy and platforms load in a non-awaited tracked task
|
||||
# to ensure device tracker setup can continue and config
|
||||
# entry integrations are not waiting for legacy device
|
||||
# tracker platforms to be set up.
|
||||
#
|
||||
hass.async_create_task(
|
||||
async_setup_legacy_integration(hass, config, tracker_future),
|
||||
eager_start=True,
|
||||
)
|
||||
return True
|
||||
|
||||
@@ -37,11 +37,7 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
discovery,
|
||||
entity_registry as er,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_time_interval,
|
||||
async_track_utc_time_change,
|
||||
@@ -204,40 +200,7 @@ def see(
|
||||
hass.services.call(DOMAIN, SERVICE_SEE, data)
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_integration(hass: HomeAssistant, config: ConfigType) -> None:
|
||||
"""Set up the legacy integration."""
|
||||
# The tracker is loaded in the _async_setup_integration task so
|
||||
# we create a future to avoid waiting on it here so that only
|
||||
# async_platform_discovered will have to wait in the rare event
|
||||
# a custom component still uses the legacy device tracker discovery.
|
||||
tracker_future: asyncio.Future[DeviceTracker] = hass.loop.create_future()
|
||||
|
||||
async def async_platform_discovered(
|
||||
p_type: str, info: dict[str, Any] | None
|
||||
) -> None:
|
||||
"""Load a platform."""
|
||||
platform = await async_create_platform_type(hass, config, p_type, {})
|
||||
|
||||
if platform is None or platform.type != PLATFORM_TYPE_LEGACY:
|
||||
return
|
||||
|
||||
tracker = await tracker_future
|
||||
await platform.async_setup_legacy(hass, tracker, info)
|
||||
|
||||
discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered)
|
||||
#
|
||||
# Legacy and platforms load in a non-awaited tracked task
|
||||
# to ensure device tracker setup can continue and config
|
||||
# entry integrations are not waiting for legacy device
|
||||
# tracker platforms to be set up.
|
||||
#
|
||||
hass.async_create_task(
|
||||
_async_setup_integration(hass, config, tracker_future), eager_start=True
|
||||
)
|
||||
|
||||
|
||||
async def _async_setup_integration(
|
||||
async def async_setup_integration(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
tracker_future: asyncio.Future[DeviceTracker],
|
||||
|
||||
@@ -31,7 +31,6 @@ class DownloaderConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
else:
|
||||
return self.async_create_entry(title=DEFAULT_NAME, data=user_input)
|
||||
|
||||
# pylint: disable-next=home-assistant-config-flow-field-not-translated
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
|
||||
@@ -52,5 +52,4 @@ class EGPSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
else:
|
||||
return self.async_abort(reason="no_device")
|
||||
|
||||
# pylint: disable-next=home-assistant-config-flow-field-not-translated
|
||||
return self.async_show_form(step_id="user", data_schema=vol.Schema(data_schema))
|
||||
|
||||
@@ -277,7 +277,6 @@ class FluxLedConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
# Check if there is at least one device
|
||||
if not devices_name:
|
||||
return self.async_abort(reason="no_devices_found")
|
||||
# pylint: disable-next=home-assistant-config-flow-field-not-translated
|
||||
return self.async_show_form(
|
||||
step_id="pick_device",
|
||||
data_schema=vol.Schema({vol.Required(CONF_DEVICE): vol.In(devices_name)}),
|
||||
|
||||
@@ -125,7 +125,6 @@ class Gogogate2FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
CONF_DEVICE: DEVICE_NAMES[self._device_type],
|
||||
CONF_IP_ADDRESS: self._ip_address,
|
||||
}
|
||||
# pylint: disable-next=home-assistant-config-flow-field-not-translated
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
|
||||
@@ -140,7 +140,6 @@ class HueFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
if not self.discovered_bridges:
|
||||
return await self.async_step_manual()
|
||||
|
||||
# pylint: disable-next=home-assistant-config-flow-field-not-translated
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(
|
||||
|
||||
@@ -12,6 +12,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/indevolt",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "silver",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["indevolt-api==1.8.1"]
|
||||
}
|
||||
|
||||
@@ -75,7 +75,6 @@ class IslamicPrayerFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
CONF_LATITUDE: self.hass.config.latitude,
|
||||
CONF_LONGITUDE: self.hass.config.longitude,
|
||||
}
|
||||
# pylint: disable-next=home-assistant-config-flow-field-not-translated
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
|
||||
@@ -100,7 +100,6 @@ class LaCrosseViewConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
if not user_input:
|
||||
_LOGGER.debug("Showing initial location selection")
|
||||
# pylint: disable-next=home-assistant-config-flow-field-not-translated
|
||||
return self.async_show_form(
|
||||
step_id="location",
|
||||
data_schema=vol.Schema(
|
||||
|
||||
@@ -75,7 +75,6 @@ class LookinFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
data={CONF_HOST: host},
|
||||
)
|
||||
|
||||
# pylint: disable-next=home-assistant-config-flow-field-not-translated
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
|
||||
|
||||
@@ -77,7 +77,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: LunatoneConfigEntry) ->
|
||||
await coordinator_info.async_config_entry_first_refresh()
|
||||
|
||||
if info_api.data is None or info_api.serial_number is None:
|
||||
# pylint: disable-next=home-assistant-exception-translation-key-missing
|
||||
raise ConfigEntryError(
|
||||
translation_domain=DOMAIN, translation_key="missing_device_info"
|
||||
)
|
||||
|
||||
@@ -207,7 +207,6 @@ class LunatoneLight(
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
|
||||
# pylint: disable-next=home-assistant-missing-has-entity-name
|
||||
class LunatoneLineBroadcastLight(
|
||||
CoordinatorEntity[LunatoneInfoDataUpdateCoordinator], LightEntity
|
||||
):
|
||||
@@ -217,6 +216,8 @@ class LunatoneLineBroadcastLight(
|
||||
|
||||
_attr_assumed_state = True
|
||||
_attr_color_mode = ColorMode.BRIGHTNESS
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
_attr_supported_color_modes = {ColorMode.BRIGHTNESS}
|
||||
|
||||
def __init__(
|
||||
|
||||
@@ -35,5 +35,10 @@
|
||||
"description": "Enter the URL of your Lunatone device.\nHome Assistant will use this address to connect to the device API."
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"missing_device_info": {
|
||||
"message": "Unable to read device information. Please verify the device's network connection."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,6 @@ class MelnorConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if not addresses:
|
||||
return self.async_abort(reason="no_devices_found")
|
||||
|
||||
# pylint: disable-next=home-assistant-config-flow-field-not-translated
|
||||
return self.async_show_form(
|
||||
step_id="pick_device",
|
||||
data_schema=vol.Schema({vol.Required(CONF_ADDRESS): vol.In(addresses)}),
|
||||
|
||||
@@ -11,7 +11,7 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.const import CONF_COUNTRY, CONF_LANGUAGE, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
@@ -22,10 +22,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTRIBUTION = "Information provided by MeteoAlarm"
|
||||
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
CONF_COUNTRY = "country"
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
CONF_LANGUAGE = "language"
|
||||
CONF_PROVINCE = "province"
|
||||
|
||||
DEFAULT_NAME = "meteoalarm"
|
||||
|
||||
@@ -24,7 +24,6 @@ class MeteoclimaticFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
if user_input is None:
|
||||
user_input = {}
|
||||
|
||||
# pylint: disable-next=home-assistant-config-flow-field-not-translated
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
|
||||
@@ -117,7 +117,6 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
},
|
||||
)
|
||||
|
||||
# pylint: disable-next=home-assistant-config-flow-field-not-translated
|
||||
return self.async_show_form(
|
||||
step_id="confirm",
|
||||
data_schema=vol.Schema(
|
||||
|
||||
@@ -23,7 +23,7 @@ from music_assistant_models.errors import (
|
||||
from music_assistant_models.player import Player
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP, Platform
|
||||
from homeassistant.const import CONF_TOKEN, CONF_URL, EVENT_HOMEASSISTANT_STOP, Platform
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
@@ -38,7 +38,7 @@ from homeassistant.helpers.issue_registry import (
|
||||
async_delete_issue,
|
||||
)
|
||||
|
||||
from .const import ATTR_CONF_EXPOSE_PLAYER_TO_HA, CONF_TOKEN, DOMAIN, LOGGER
|
||||
from .const import ATTR_CONF_EXPOSE_PLAYER_TO_HA, DOMAIN, LOGGER
|
||||
from .helpers import get_music_assistant_client
|
||||
from .services import register_actions
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ from homeassistant.config_entries import (
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
)
|
||||
from homeassistant.const import CONF_URL
|
||||
from homeassistant.const import CONF_TOKEN, CONF_URL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
@@ -31,13 +31,7 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
from homeassistant.helpers.service_info.hassio import HassioServiceInfo
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
|
||||
from .const import (
|
||||
AUTH_SCHEMA_VERSION,
|
||||
CONF_TOKEN,
|
||||
DOMAIN,
|
||||
HASSIO_DISCOVERY_SCHEMA_VERSION,
|
||||
LOGGER,
|
||||
)
|
||||
from .const import AUTH_SCHEMA_VERSION, DOMAIN, HASSIO_DISCOVERY_SCHEMA_VERSION, LOGGER
|
||||
|
||||
DEFAULT_TITLE = "Music Assistant"
|
||||
DEFAULT_URL = "http://mass.local:8095"
|
||||
|
||||
@@ -12,9 +12,6 @@ AUTH_SCHEMA_VERSION = 28
|
||||
# Schema version where hassio discovery support was added
|
||||
HASSIO_DISCOVERY_SCHEMA_VERSION = 28
|
||||
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
CONF_TOKEN = "token"
|
||||
|
||||
ATTR_IS_GROUP = "is_group"
|
||||
ATTR_GROUP_MEMBERS = "group_members"
|
||||
ATTR_GROUP_PARENTS = "group_parents"
|
||||
|
||||
@@ -206,7 +206,6 @@ class PlaatoOptionsFlowHandler(OptionsFlow):
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
# pylint: disable-next=home-assistant-options-flow-field-not-translated
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from aiopvpc import DEFAULT_POWER_KW, PVPCData
|
||||
from esios_api import DEFAULT_POWER_KW, PVPCData
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
@@ -63,9 +63,10 @@ class TariffSelectorConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if user_input is not None:
|
||||
await self.async_set_unique_id(user_input[ATTR_TARIFF])
|
||||
self._abort_if_unique_id_configured()
|
||||
calc_name = f"{DEFAULT_NAME} - {user_input[ATTR_TARIFF]}"
|
||||
if not user_input[CONF_USE_API_TOKEN]:
|
||||
return self.async_create_entry(
|
||||
title=DEFAULT_NAME,
|
||||
title=calc_name,
|
||||
data={
|
||||
ATTR_TARIFF: user_input[ATTR_TARIFF],
|
||||
ATTR_POWER: user_input[ATTR_POWER],
|
||||
@@ -74,7 +75,7 @@ class TariffSelectorConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
},
|
||||
)
|
||||
|
||||
self._name = DEFAULT_NAME
|
||||
self._name = calc_name
|
||||
self._tariff = user_input[ATTR_TARIFF]
|
||||
self._power = user_input[ATTR_POWER]
|
||||
self._power_p3 = user_input[ATTR_POWER_P3]
|
||||
@@ -150,7 +151,7 @@ class TariffSelectorConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle re-authentication with ESIOS Token."""
|
||||
self._api_token = entry_data.get(CONF_API_TOKEN)
|
||||
self._use_api_token = self._api_token is not None
|
||||
self._name = DEFAULT_NAME
|
||||
self._name = f"{DEFAULT_NAME} - {entry_data[ATTR_TARIFF]}"
|
||||
self._tariff = entry_data[ATTR_TARIFF]
|
||||
self._power = entry_data[ATTR_POWER]
|
||||
self._power_p3 = entry_data[ATTR_POWER_P3]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Constant values for pvpc_hourly_pricing."""
|
||||
|
||||
from aiopvpc.const import TARIFFS
|
||||
from esios_api.const import TARIFFS
|
||||
import voluptuous as vol
|
||||
|
||||
DOMAIN = "pvpc_hourly_pricing"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from aiopvpc import BadApiTokenAuthError, EsiosApiData, PVPCData
|
||||
from esios_api import BadApiTokenAuthError, EsiosApiData, PVPCData
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_TOKEN
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Helper functions to relate sensors keys and unique ids."""
|
||||
|
||||
from aiopvpc.const import (
|
||||
from esios_api.const import (
|
||||
ALL_SENSORS,
|
||||
KEY_INJECTION,
|
||||
KEY_MAG,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"domain": "pvpc_hourly_pricing",
|
||||
"name": "Spain electricity hourly pricing (PVPC)",
|
||||
"codeowners": ["@azogue"],
|
||||
"codeowners": ["@azogue", "@chiro79"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/pvpc_hourly_pricing",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aiopvpc"],
|
||||
"requirements": ["aiopvpc==4.3.1"]
|
||||
"loggers": ["esios_api"],
|
||||
"requirements": ["esios_api==4.4.0"]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ from datetime import datetime
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiopvpc.const import KEY_INJECTION, KEY_MAG, KEY_OMIE, KEY_PVPC
|
||||
from esios_api.const import KEY_INJECTION, KEY_MAG, KEY_OMIE, KEY_PVPC
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
|
||||
@@ -64,7 +64,6 @@ class SimpleFinConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
data={CONF_ACCESS_URL: user_input[CONF_ACCESS_URL]},
|
||||
)
|
||||
|
||||
# pylint: disable-next=home-assistant-config-flow-field-not-translated
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
|
||||
@@ -66,7 +66,6 @@ class SmhiFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
CONF_LATITUDE: self.hass.config.latitude,
|
||||
CONF_LONGITUDE: self.hass.config.longitude,
|
||||
}
|
||||
# pylint: disable-next=home-assistant-config-flow-field-not-translated
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
|
||||
@@ -110,7 +110,6 @@ class SnoozConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if not self._discovered_devices:
|
||||
return self.async_abort(reason="no_devices_found")
|
||||
|
||||
# pylint: disable-next=home-assistant-config-flow-field-not-translated
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
|
||||
@@ -191,11 +191,11 @@ class SonosAudioInputFormatSensorEntity(SonosPollingEntity, SensorEntity):
|
||||
"""Provide a stub for required ABC method."""
|
||||
|
||||
|
||||
# pylint: disable-next=home-assistant-missing-has-entity-name
|
||||
class SonosFavoritesEntity(SensorEntity):
|
||||
"""Representation of a Sonos favorites info entity."""
|
||||
|
||||
_attr_entity_registry_enabled_default = False
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = "Sonos favorites"
|
||||
_attr_translation_key = "favorites"
|
||||
_attr_native_unit_of_measurement = "items"
|
||||
|
||||
@@ -38,12 +38,12 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
# pylint: disable-next=home-assistant-missing-has-entity-name
|
||||
class OmadaClientScannerEntity(
|
||||
CoordinatorEntity[OmadaClientsCoordinator], ScannerEntity
|
||||
):
|
||||
"""Entity for a client connected to the Omada network."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_client_details: OmadaWirelessClient | None = None
|
||||
|
||||
def __init__(
|
||||
|
||||
@@ -75,7 +75,6 @@ class TuyaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
) -> ConfigFlowResult:
|
||||
"""Step scan."""
|
||||
if user_input is None:
|
||||
# pylint: disable-next=home-assistant-config-flow-field-not-translated
|
||||
return self.async_show_form(
|
||||
step_id="scan",
|
||||
data_schema=vol.Schema(
|
||||
@@ -100,7 +99,6 @@ class TuyaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if not ret:
|
||||
# Try to get a new QR code on failure
|
||||
await self.__async_get_qr_code(self.__user_code)
|
||||
# pylint: disable-next=home-assistant-config-flow-field-not-translated
|
||||
return self.async_show_form(
|
||||
step_id="scan",
|
||||
errors={"base": "login_error"},
|
||||
|
||||
@@ -1,439 +0,0 @@
|
||||
"""Checker for missing config/options/subentry flow form translations.
|
||||
|
||||
When a flow calls ``async_show_form`` with a ``data_schema``, every
|
||||
field in the schema needs a corresponding translation entry in
|
||||
``strings.json``.
|
||||
|
||||
The expected translation paths are::
|
||||
|
||||
config.step.{step_id}.data.{field_name}
|
||||
config.step.{step_id}.sections.{section_key}.data.{field_name}
|
||||
options.step.{step_id}.data.{field_name}
|
||||
options.step.{step_id}.sections.{section_key}.data.{field_name}
|
||||
config_subentries.{subentry_type}.step.{step_id}.data.{field_name}
|
||||
|
||||
- ``W7420``: Missing config flow field translation
|
||||
- ``W7421``: Missing options flow field translation
|
||||
- ``W7422``: Missing subentry flow field translation
|
||||
"""
|
||||
|
||||
import astroid
|
||||
from astroid import nodes
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.lint import PyLinter
|
||||
|
||||
from pylint_home_assistant.helpers.module_info import get_module_platform
|
||||
from pylint_home_assistant.helpers.translations import load_translations
|
||||
|
||||
_InferenceError = astroid.exceptions.InferenceError
|
||||
|
||||
|
||||
def _extract_step_id(call: nodes.Call) -> str | None:
|
||||
"""Extract step_id from an async_show_form call.
|
||||
|
||||
Falls back to the enclosing method name if step_id is omitted
|
||||
(e.g., ``async_step_user`` -> ``"user"``).
|
||||
"""
|
||||
for kw in call.keywords:
|
||||
if kw.arg == "step_id":
|
||||
if isinstance(kw.value, nodes.Const):
|
||||
return str(kw.value.value)
|
||||
try:
|
||||
for inferred in kw.value.infer():
|
||||
if isinstance(inferred, nodes.Const) and isinstance(
|
||||
inferred.value, str
|
||||
):
|
||||
return str(inferred.value)
|
||||
except _InferenceError:
|
||||
pass
|
||||
return None
|
||||
|
||||
# No step_id keyword: infer from enclosing async_step_* method
|
||||
current = call.parent
|
||||
while current is not None:
|
||||
if isinstance(current, nodes.FunctionDef):
|
||||
if current.name.startswith("async_step_"):
|
||||
return str(current.name).removeprefix("async_step_")
|
||||
break
|
||||
current = current.parent
|
||||
return None
|
||||
|
||||
|
||||
# Result types for schema extraction
|
||||
|
||||
|
||||
class _Field:
|
||||
"""A regular form field."""
|
||||
|
||||
__slots__ = ("name",)
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
self.name = name
|
||||
|
||||
|
||||
class _Section:
|
||||
"""A section containing nested fields."""
|
||||
|
||||
__slots__ = ("fields", "key")
|
||||
|
||||
def __init__(self, key: str, fields: list[str]) -> None:
|
||||
self.key = key
|
||||
self.fields = fields
|
||||
|
||||
|
||||
def _extract_schema_items(call: nodes.Call) -> list[_Field | _Section]:
|
||||
"""Extract fields and sections from the data_schema keyword argument."""
|
||||
schema_node = None
|
||||
for kw in call.keywords:
|
||||
if kw.arg == "data_schema":
|
||||
schema_node = kw.value
|
||||
break
|
||||
|
||||
if schema_node is None:
|
||||
return []
|
||||
|
||||
return _extract_items_from_node(schema_node)
|
||||
|
||||
|
||||
def _extract_items_from_node(
|
||||
node: nodes.NodeNG,
|
||||
) -> list[_Field | _Section]:
|
||||
"""Recursively extract fields and sections from a schema node."""
|
||||
items: list[_Field | _Section] = []
|
||||
|
||||
# vol.Schema({...}) or similar call wrapping a dict
|
||||
if isinstance(node, nodes.Call) and node.args:
|
||||
return _extract_items_from_node(node.args[0])
|
||||
|
||||
# Direct dict literal
|
||||
if isinstance(node, nodes.Dict):
|
||||
for key, value in node.items:
|
||||
if isinstance(key, nodes.DictUnpack):
|
||||
try:
|
||||
for inferred in value.infer():
|
||||
if isinstance(inferred, nodes.Dict):
|
||||
items.extend(_extract_items_from_node(inferred))
|
||||
except _InferenceError:
|
||||
pass
|
||||
continue
|
||||
|
||||
name = _resolve_field_name(key)
|
||||
if name is None:
|
||||
continue
|
||||
|
||||
# Check if the value is a section(...) call
|
||||
section_fields = _extract_section_fields(value)
|
||||
if section_fields is not None:
|
||||
items.append(_Section(name, section_fields))
|
||||
else:
|
||||
items.append(_Field(name))
|
||||
return items
|
||||
|
||||
# Variable reference: try to infer to a Call or Dict
|
||||
try:
|
||||
for inferred in node.infer():
|
||||
if isinstance(inferred, (nodes.Call, nodes.Dict)):
|
||||
return _extract_items_from_node(inferred)
|
||||
except _InferenceError:
|
||||
pass
|
||||
|
||||
return items
|
||||
|
||||
|
||||
def _extract_section_fields(node: nodes.NodeNG) -> list[str] | None:
|
||||
"""Extract field names from a section(...) call.
|
||||
|
||||
Returns a list of field names if the node is a section() call,
|
||||
or None if it's not a section.
|
||||
"""
|
||||
if not isinstance(node, nodes.Call):
|
||||
return None
|
||||
# Match section(...) or data_entry_flow.section(...)
|
||||
if isinstance(node.func, nodes.Name):
|
||||
if node.func.name != "section":
|
||||
return None
|
||||
elif isinstance(node.func, nodes.Attribute):
|
||||
if node.func.attrname != "section":
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
if not node.args:
|
||||
return None
|
||||
|
||||
# section(vol.Schema({...}), ...) - first arg is the schema
|
||||
inner_items = _extract_items_from_node(node.args[0])
|
||||
return [item.name for item in inner_items if isinstance(item, _Field)]
|
||||
|
||||
|
||||
def _resolve_field_name(node: nodes.NodeNG) -> str | None:
|
||||
"""Resolve a schema key to a field name string."""
|
||||
if isinstance(node, nodes.Call) and isinstance(node.func, nodes.Attribute):
|
||||
if node.func.attrname in ("Required", "Optional") and node.args:
|
||||
return _resolve_field_name(node.args[0])
|
||||
|
||||
if isinstance(node, nodes.Const) and isinstance(node.value, str):
|
||||
return node.value
|
||||
|
||||
try:
|
||||
for inferred in node.infer():
|
||||
if isinstance(inferred, nodes.Const) and isinstance(inferred.value, str):
|
||||
return inferred.value
|
||||
except _InferenceError:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _find_enclosing_class(node: nodes.Call) -> nodes.ClassDef | None:
|
||||
"""Find the enclosing class definition for a call node."""
|
||||
current = node.parent
|
||||
while current is not None:
|
||||
if isinstance(current, nodes.ClassDef):
|
||||
return current
|
||||
current = current.parent
|
||||
return None
|
||||
|
||||
|
||||
def _get_flow_type(class_node: nodes.ClassDef) -> str | None:
|
||||
"""Determine flow type from a class definition."""
|
||||
for base in class_node.bases:
|
||||
match base:
|
||||
case nodes.Name(name=name):
|
||||
if "SubentryFlow" in name:
|
||||
return "subentry"
|
||||
if "OptionsFlow" in name:
|
||||
return "options"
|
||||
if "ConfigFlow" in name or "FlowHandler" in name:
|
||||
return "config"
|
||||
case nodes.Attribute(attrname=name):
|
||||
if "SubentryFlow" in name:
|
||||
return "subentry"
|
||||
if "OptionsFlow" in name:
|
||||
return "options"
|
||||
if "ConfigFlow" in name or "FlowHandler" in name:
|
||||
return "config"
|
||||
|
||||
try:
|
||||
for ancestor in class_node.ancestors():
|
||||
if "SubentryFlow" in ancestor.name:
|
||||
return "subentry"
|
||||
if "OptionsFlow" in ancestor.name:
|
||||
return "options"
|
||||
if "ConfigFlow" in ancestor.name:
|
||||
return "config"
|
||||
except _InferenceError:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _resolve_subentry_types(module: nodes.Module, handler_class_name: str) -> list[str]:
|
||||
"""Resolve subentry type names for a handler class."""
|
||||
subentry_types: list[str] = []
|
||||
|
||||
for node in module.body:
|
||||
if not isinstance(node, nodes.ClassDef):
|
||||
continue
|
||||
if _get_flow_type(node) != "config":
|
||||
continue
|
||||
|
||||
for method in node.mymethods():
|
||||
if method.name != "async_get_supported_subentry_types":
|
||||
continue
|
||||
for child in method.body:
|
||||
if not isinstance(child, nodes.Return):
|
||||
continue
|
||||
if not isinstance(child.value, nodes.Dict):
|
||||
continue
|
||||
for key, value in child.value.items:
|
||||
if isinstance(key, nodes.DictUnpack):
|
||||
continue
|
||||
key_str = _resolve_field_name(key)
|
||||
if key_str and isinstance(value, nodes.Name):
|
||||
if value.name == handler_class_name:
|
||||
subentry_types.append(key_str)
|
||||
|
||||
return subentry_types
|
||||
|
||||
|
||||
class ConfigFlowTranslationsChecker(BaseChecker):
|
||||
"""Checker for missing flow form translations."""
|
||||
|
||||
name = "home_assistant_config_flow_translations"
|
||||
priority = -1
|
||||
msgs = {
|
||||
"W7420": (
|
||||
"Form field '%s' in step '%s' is missing a translation in "
|
||||
"strings.json (expected at %s)",
|
||||
"home-assistant-config-flow-field-not-translated",
|
||||
"Used when a config flow form field does not have a "
|
||||
"corresponding translation in strings.json.",
|
||||
),
|
||||
"W7421": (
|
||||
"Form field '%s' in step '%s' is missing a translation in "
|
||||
"strings.json (expected at %s)",
|
||||
"home-assistant-options-flow-field-not-translated",
|
||||
"Used when an options flow form field does not have a "
|
||||
"corresponding translation in strings.json.",
|
||||
),
|
||||
"W7422": (
|
||||
"Form field '%s' in step '%s' is missing a translation in "
|
||||
"strings.json (expected at %s)",
|
||||
"home-assistant-subentry-flow-field-not-translated",
|
||||
"Used when a subentry flow form field does not have a "
|
||||
"corresponding translation in strings.json.",
|
||||
),
|
||||
}
|
||||
options = ()
|
||||
|
||||
_translations: dict | None
|
||||
_is_config_flow: bool
|
||||
_module_node: nodes.Module | None
|
||||
|
||||
def visit_module(self, node: nodes.Module) -> None:
|
||||
"""Load translations for config_flow modules."""
|
||||
platform = get_module_platform(node.name)
|
||||
self._is_config_flow = platform == "config_flow"
|
||||
self._translations = None
|
||||
self._module_node = None
|
||||
if self._is_config_flow:
|
||||
self._translations = load_translations(node)
|
||||
self._module_node = node
|
||||
|
||||
def visit_call(self, node: nodes.Call) -> None:
|
||||
"""Check async_show_form calls for translated fields."""
|
||||
if not self._is_config_flow or self._translations is None:
|
||||
return
|
||||
|
||||
if not isinstance(node.func, nodes.Attribute):
|
||||
return
|
||||
if node.func.attrname != "async_show_form":
|
||||
return
|
||||
|
||||
step_id = _extract_step_id(node)
|
||||
if step_id is None:
|
||||
return
|
||||
|
||||
schema_items = _extract_schema_items(node)
|
||||
if not schema_items:
|
||||
return
|
||||
|
||||
class_node = _find_enclosing_class(node)
|
||||
if class_node is None:
|
||||
return
|
||||
|
||||
flow_type = _get_flow_type(class_node) or "config"
|
||||
|
||||
if flow_type == "config":
|
||||
self._check_flow(
|
||||
node,
|
||||
step_id,
|
||||
schema_items,
|
||||
"config",
|
||||
"home-assistant-config-flow-field-not-translated",
|
||||
)
|
||||
elif flow_type == "options":
|
||||
self._check_flow(
|
||||
node,
|
||||
step_id,
|
||||
schema_items,
|
||||
"options",
|
||||
"home-assistant-options-flow-field-not-translated",
|
||||
)
|
||||
elif flow_type == "subentry":
|
||||
self._check_subentry_flow(node, step_id, schema_items, class_node)
|
||||
|
||||
def _check_flow(
|
||||
self,
|
||||
node: nodes.Call,
|
||||
step_id: str,
|
||||
schema_items: list[_Field | _Section],
|
||||
flow_key: str,
|
||||
msg_id: str,
|
||||
) -> None:
|
||||
"""Check fields against translations for config/options flows."""
|
||||
assert self._translations is not None
|
||||
step_trans = (
|
||||
self._translations.get(flow_key, {}).get("step", {}).get(step_id, {})
|
||||
)
|
||||
data_trans = step_trans.get("data", {})
|
||||
sections_trans = step_trans.get("sections", {})
|
||||
|
||||
for item in schema_items:
|
||||
if isinstance(item, _Field):
|
||||
if item.name not in data_trans:
|
||||
path = f"{flow_key}.step.{step_id}.data.{item.name}"
|
||||
self.add_message(
|
||||
msg_id,
|
||||
node=node,
|
||||
args=(item.name, step_id, path),
|
||||
)
|
||||
elif isinstance(item, _Section):
|
||||
section_data = sections_trans.get(item.key, {}).get("data", {})
|
||||
for field in item.fields:
|
||||
if field not in section_data:
|
||||
path = f"{flow_key}.step.{step_id}.sections.{item.key}.data.{field}"
|
||||
self.add_message(
|
||||
msg_id,
|
||||
node=node,
|
||||
args=(field, step_id, path),
|
||||
)
|
||||
|
||||
def _check_subentry_flow(
|
||||
self,
|
||||
node: nodes.Call,
|
||||
step_id: str,
|
||||
schema_items: list[_Field | _Section],
|
||||
class_node: nodes.ClassDef,
|
||||
) -> None:
|
||||
"""Check subentry flow fields against translations."""
|
||||
if self._module_node is None:
|
||||
return
|
||||
|
||||
subentry_types = _resolve_subentry_types(self._module_node, class_node.name)
|
||||
if not subentry_types:
|
||||
return
|
||||
|
||||
assert self._translations is not None
|
||||
config_subentries = self._translations.get("config_subentries", {})
|
||||
|
||||
sub0 = subentry_types[0]
|
||||
for item in schema_items:
|
||||
if isinstance(item, _Section):
|
||||
for field in item.fields:
|
||||
if not any(
|
||||
field
|
||||
in config_subentries.get(st, {})
|
||||
.get("step", {})
|
||||
.get(step_id, {})
|
||||
.get("sections", {})
|
||||
.get(item.key, {})
|
||||
.get("data", {})
|
||||
for st in subentry_types
|
||||
):
|
||||
path = f"config_subentries.{sub0}.step.{step_id}.sections.{item.key}.data.{field}"
|
||||
self.add_message(
|
||||
"home-assistant-subentry-flow-field-not-translated",
|
||||
node=node,
|
||||
args=(field, step_id, path),
|
||||
)
|
||||
elif isinstance(item, _Field):
|
||||
if not any(
|
||||
item.name
|
||||
in config_subentries.get(st, {})
|
||||
.get("step", {})
|
||||
.get(step_id, {})
|
||||
.get("data", {})
|
||||
for st in subentry_types
|
||||
):
|
||||
path = f"config_subentries.{sub0}.step.{step_id}.data.{item.name}"
|
||||
self.add_message(
|
||||
"home-assistant-subentry-flow-field-not-translated",
|
||||
node=node,
|
||||
args=(item.name, step_id, path),
|
||||
)
|
||||
|
||||
|
||||
def register(linter: PyLinter) -> None:
|
||||
"""Register the checker."""
|
||||
linter.register_checker(ConfigFlowTranslationsChecker(linter))
|
||||
Generated
+3
-3
@@ -374,9 +374,6 @@ aiopurpleair==2025.08.1
|
||||
# homeassistant.components.hunterdouglas_powerview
|
||||
aiopvapi==3.3.0
|
||||
|
||||
# homeassistant.components.pvpc_hourly_pricing
|
||||
aiopvpc==4.3.1
|
||||
|
||||
# homeassistant.components.lidarr
|
||||
# homeassistant.components.radarr
|
||||
# homeassistant.components.sonarr
|
||||
@@ -951,6 +948,9 @@ epson-projector==0.6.0
|
||||
# homeassistant.components.eq3btsmart
|
||||
eq3btsmart==2.3.0
|
||||
|
||||
# homeassistant.components.pvpc_hourly_pricing
|
||||
esios_api==4.4.0
|
||||
|
||||
# homeassistant.components.esphome
|
||||
esphome-dashboard-api==1.3.0
|
||||
|
||||
|
||||
@@ -203,7 +203,6 @@ FORBIDDEN_PACKAGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = {
|
||||
"opengarage": {"open-garage": {"async-timeout"}},
|
||||
"overkiz": {"pyoverkiz": {"backoff"}},
|
||||
"prosegur": {"pyprosegur": {"backoff"}},
|
||||
"pvpc_hourly_pricing": {"aiopvpc": {"async-timeout"}},
|
||||
"radio_browser": {"radios": {"backoff"}},
|
||||
"remote_rpi_gpio": {
|
||||
# https://github.com/waveform80/colorzero/issues/9
|
||||
|
||||
@@ -8,7 +8,12 @@ from unittest.mock import call, patch
|
||||
import pytest
|
||||
|
||||
from homeassistant.components import device_tracker, zone
|
||||
from homeassistant.components.device_tracker import SourceType, const, legacy
|
||||
from homeassistant.components.device_tracker import (
|
||||
SourceType,
|
||||
TrackerEntity,
|
||||
const,
|
||||
legacy,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_PICTURE,
|
||||
ATTR_FRIENDLY_NAME,
|
||||
@@ -19,11 +24,15 @@ from homeassistant.const import (
|
||||
CONF_PLATFORM,
|
||||
STATE_HOME,
|
||||
STATE_NOT_HOME,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, State, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.discovery import DiscoveryInfoType
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.json import JSONEncoder
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
@@ -31,9 +40,13 @@ from . import common
|
||||
from .common import MockScanner, mock_legacy_device_tracker_setup
|
||||
|
||||
from tests.common import (
|
||||
MockModule,
|
||||
MockPlatform,
|
||||
RegistryEntryWithDefaults,
|
||||
assert_setup_component,
|
||||
async_fire_time_changed,
|
||||
mock_integration,
|
||||
mock_platform,
|
||||
mock_registry,
|
||||
mock_restore_cache,
|
||||
patch_yaml_files,
|
||||
@@ -729,3 +742,82 @@ def test_see_schema_allowing_ios_calls() -> None:
|
||||
"hostname": "beer",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def test_modern_platform_setup(hass: HomeAssistant) -> None:
|
||||
"""Test modern platform setup."""
|
||||
|
||||
test_domain = "test"
|
||||
|
||||
entity1 = TrackerEntity()
|
||||
entity1.entity_id = "device_tracker.test1"
|
||||
entity1._attr_source_type = SourceType.ROUTER
|
||||
|
||||
entity2 = TrackerEntity()
|
||||
entity2.entity_id = "device_tracker.test2"
|
||||
entity2._attr_location_name = "home"
|
||||
entity2._attr_location_accuracy = 1
|
||||
entity2._attr_latitude = 10.0
|
||||
entity2._attr_longitude = 5.0
|
||||
entity2._attr_source_type = SourceType.GPS
|
||||
|
||||
entity3 = TrackerEntity()
|
||||
entity3.entity_id = "device_tracker.test3"
|
||||
entity3._attr_location_name = "not_home"
|
||||
entity3._attr_source_type = SourceType.ROUTER
|
||||
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> bool:
|
||||
async_add_entities([entity1, entity2, entity3])
|
||||
return True
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
hass.async_create_task(
|
||||
discovery.async_load_platform(
|
||||
hass, "device_tracker", test_domain, {}, config
|
||||
)
|
||||
)
|
||||
return True
|
||||
|
||||
mock_integration(
|
||||
hass,
|
||||
MockModule(test_domain, async_setup=async_setup),
|
||||
)
|
||||
mock_platform(
|
||||
hass,
|
||||
f"{test_domain}.device_tracker",
|
||||
MockPlatform(async_setup_platform=async_setup_platform),
|
||||
)
|
||||
|
||||
await async_setup_component(hass, "homeassistant", {})
|
||||
await async_setup_component(hass, "device_tracker", {})
|
||||
await async_setup_component(hass, test_domain, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity1.entity_id)
|
||||
assert state
|
||||
assert state.state == STATE_UNKNOWN
|
||||
assert state.attributes == {"in_zones": [], "source_type": SourceType.ROUTER}
|
||||
|
||||
state = hass.states.get(entity2.entity_id)
|
||||
assert state
|
||||
assert state.state == STATE_HOME
|
||||
assert state.attributes == {
|
||||
"in_zones": [],
|
||||
"source_type": SourceType.GPS,
|
||||
"latitude": 10.0,
|
||||
"longitude": 5.0,
|
||||
"gps_accuracy": 1,
|
||||
}
|
||||
|
||||
state = hass.states.get(entity3.entity_id)
|
||||
assert state
|
||||
assert state.state == STATE_NOT_HOME
|
||||
assert state.attributes == {
|
||||
"in_zones": [],
|
||||
"source_type": SourceType.ROUTER,
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
'domain': 'light',
|
||||
'entity_category': None,
|
||||
'entity_id': 'light.dali_line_0',
|
||||
'has_entity_name': False,
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
@@ -79,7 +79,7 @@
|
||||
'domain': 'light',
|
||||
'entity_category': None,
|
||||
'entity_id': 'light.dali_line_1',
|
||||
'has_entity_name': False,
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
|
||||
@@ -22,7 +22,6 @@ from homeassistant.components.music_assistant.config_flow import (
|
||||
)
|
||||
from homeassistant.components.music_assistant.const import (
|
||||
AUTH_SCHEMA_VERSION,
|
||||
CONF_TOKEN,
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
)
|
||||
@@ -34,6 +33,7 @@ from homeassistant.config_entries import (
|
||||
SOURCE_ZEROCONF,
|
||||
ConfigEntryState,
|
||||
)
|
||||
from homeassistant.const import CONF_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers.service_info.hassio import HassioServiceInfo
|
||||
|
||||
@@ -1,731 +0,0 @@
|
||||
"""Tests for the config flow translations checker."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import astroid
|
||||
from pylint.testutils import UnittestLinter
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
from pylint_home_assistant.checkers.flow_translations import (
|
||||
ConfigFlowTranslationsChecker,
|
||||
)
|
||||
from pylint_home_assistant.helpers.translations import clear_translations_cache
|
||||
import pytest
|
||||
|
||||
from . import assert_no_messages
|
||||
|
||||
|
||||
@pytest.fixture(name="flow_translations_checker")
|
||||
def flow_translations_checker_fixture(
|
||||
linter: UnittestLinter,
|
||||
) -> ConfigFlowTranslationsChecker:
|
||||
"""Fixture to provide a config flow translations checker."""
|
||||
clear_translations_cache()
|
||||
return ConfigFlowTranslationsChecker(linter)
|
||||
|
||||
|
||||
def _make_integration(tmp_path: Path, strings: dict | None = None) -> Path:
|
||||
"""Create a fake integration with optional strings.json."""
|
||||
integration_dir = tmp_path / "homeassistant" / "components" / "test_int"
|
||||
integration_dir.mkdir(parents=True)
|
||||
if strings is not None:
|
||||
(integration_dir / "strings.json").write_text(json.dumps(strings))
|
||||
return integration_dir
|
||||
|
||||
|
||||
# --- Config flow tests ---
|
||||
|
||||
|
||||
def test_config_flow_translated_ok(
|
||||
linter: UnittestLinter,
|
||||
flow_translations_checker: ConfigFlowTranslationsChecker,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""No warning when all config flow fields have translations."""
|
||||
integration_dir = _make_integration(
|
||||
tmp_path,
|
||||
{"config": {"step": {"user": {"data": {"host": "Host", "port": "Port"}}}}},
|
||||
)
|
||||
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
class MyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
async def async_step_user(self, user_input=None):
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema({
|
||||
vol.Required("host"): str,
|
||||
vol.Optional("port"): int,
|
||||
}),
|
||||
)
|
||||
""",
|
||||
"homeassistant.components.test_int.config_flow",
|
||||
)
|
||||
root_node.file = str(integration_dir / "config_flow.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(flow_translations_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_config_flow_missing_field_flagged(
|
||||
linter: UnittestLinter,
|
||||
flow_translations_checker: ConfigFlowTranslationsChecker,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Warning when a config flow field is missing a translation."""
|
||||
integration_dir = _make_integration(
|
||||
tmp_path,
|
||||
{"config": {"step": {"user": {"data": {"host": "Host"}}}}},
|
||||
)
|
||||
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
class MyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
async def async_step_user(self, user_input=None):
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema({
|
||||
vol.Required("host"): str,
|
||||
vol.Required("missing_field"): str,
|
||||
}),
|
||||
)
|
||||
""",
|
||||
"homeassistant.components.test_int.config_flow",
|
||||
)
|
||||
root_node.file = str(integration_dir / "config_flow.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(flow_translations_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
assert messages[0].msg_id == "home-assistant-config-flow-field-not-translated"
|
||||
assert "missing_field" in messages[0].args[0]
|
||||
|
||||
|
||||
# --- Options flow tests ---
|
||||
|
||||
|
||||
def test_options_flow_translated_ok(
|
||||
linter: UnittestLinter,
|
||||
flow_translations_checker: ConfigFlowTranslationsChecker,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""No warning when options flow fields have translations."""
|
||||
integration_dir = _make_integration(
|
||||
tmp_path,
|
||||
{"options": {"step": {"init": {"data": {"interval": "Update interval"}}}}},
|
||||
)
|
||||
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
class MyOptionsFlow(OptionsFlow):
|
||||
async def async_step_init(self, user_input=None):
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema({
|
||||
vol.Required("interval"): int,
|
||||
}),
|
||||
)
|
||||
""",
|
||||
"homeassistant.components.test_int.config_flow",
|
||||
)
|
||||
root_node.file = str(integration_dir / "config_flow.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(flow_translations_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_options_flow_missing_field_flagged(
|
||||
linter: UnittestLinter,
|
||||
flow_translations_checker: ConfigFlowTranslationsChecker,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Warning when an options flow field is missing a translation."""
|
||||
integration_dir = _make_integration(
|
||||
tmp_path,
|
||||
{"options": {"step": {"init": {"data": {"interval": "Update interval"}}}}},
|
||||
)
|
||||
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
class MyOptionsFlow(OptionsFlow):
|
||||
async def async_step_init(self, user_input=None):
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema({
|
||||
vol.Required("interval"): int,
|
||||
vol.Required("missing"): str,
|
||||
}),
|
||||
)
|
||||
""",
|
||||
"homeassistant.components.test_int.config_flow",
|
||||
)
|
||||
root_node.file = str(integration_dir / "config_flow.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(flow_translations_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
assert messages[0].msg_id == "home-assistant-options-flow-field-not-translated"
|
||||
|
||||
|
||||
# --- Subentry flow tests ---
|
||||
|
||||
|
||||
def test_subentry_flow_translated_ok(
|
||||
linter: UnittestLinter,
|
||||
flow_translations_checker: ConfigFlowTranslationsChecker,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""No warning when subentry flow fields have translations."""
|
||||
integration_dir = _make_integration(
|
||||
tmp_path,
|
||||
{
|
||||
"config_subentries": {
|
||||
"my_sub": {
|
||||
"step": {"user": {"data": {"name": "Name"}}},
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
class MyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
@classmethod
|
||||
def async_get_supported_subentry_types(cls, config_entry):
|
||||
return {"my_sub": MySubentryFlow}
|
||||
|
||||
class MySubentryFlow(ConfigSubentryFlow):
|
||||
async def async_step_user(self, user_input=None):
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema({
|
||||
vol.Required("name"): str,
|
||||
}),
|
||||
)
|
||||
""",
|
||||
"homeassistant.components.test_int.config_flow",
|
||||
)
|
||||
root_node.file = str(integration_dir / "config_flow.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(flow_translations_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_subentry_flow_missing_field_flagged(
|
||||
linter: UnittestLinter,
|
||||
flow_translations_checker: ConfigFlowTranslationsChecker,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Warning when a subentry flow field is missing a translation."""
|
||||
integration_dir = _make_integration(
|
||||
tmp_path,
|
||||
{
|
||||
"config_subentries": {
|
||||
"my_sub": {
|
||||
"step": {"user": {"data": {"name": "Name"}}},
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
class MyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
@classmethod
|
||||
def async_get_supported_subentry_types(cls, config_entry):
|
||||
return {"my_sub": MySubentryFlow}
|
||||
|
||||
class MySubentryFlow(ConfigSubentryFlow):
|
||||
async def async_step_user(self, user_input=None):
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema({
|
||||
vol.Required("name"): str,
|
||||
vol.Required("missing"): str,
|
||||
}),
|
||||
)
|
||||
""",
|
||||
"homeassistant.components.test_int.config_flow",
|
||||
)
|
||||
root_node.file = str(integration_dir / "config_flow.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(flow_translations_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
assert messages[0].msg_id == "home-assistant-subentry-flow-field-not-translated"
|
||||
|
||||
|
||||
def test_subentry_shared_handler_any_type_ok(
|
||||
linter: UnittestLinter,
|
||||
flow_translations_checker: ConfigFlowTranslationsChecker,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""No warning when field exists in any of the mapped subentry types."""
|
||||
integration_dir = _make_integration(
|
||||
tmp_path,
|
||||
{
|
||||
"config_subentries": {
|
||||
"type_a": {
|
||||
"step": {"user": {"data": {"name": "Name"}}},
|
||||
},
|
||||
"type_b": {
|
||||
"step": {"user": {"data": {}}},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
class MyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
@classmethod
|
||||
def async_get_supported_subentry_types(cls, config_entry):
|
||||
return {"type_a": SharedFlow, "type_b": SharedFlow}
|
||||
|
||||
class SharedFlow(ConfigSubentryFlow):
|
||||
async def async_step_user(self, user_input=None):
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema({
|
||||
vol.Required("name"): str,
|
||||
}),
|
||||
)
|
||||
""",
|
||||
"homeassistant.components.test_int.config_flow",
|
||||
)
|
||||
root_node.file = str(integration_dir / "config_flow.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(flow_translations_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
# --- Inference tests ---
|
||||
|
||||
|
||||
def test_implicit_step_id_from_method(
|
||||
linter: UnittestLinter,
|
||||
flow_translations_checker: ConfigFlowTranslationsChecker,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Test that step_id is inferred from async_step_* method name."""
|
||||
integration_dir = _make_integration(
|
||||
tmp_path,
|
||||
{"config": {"step": {"user": {"data": {"host": "Host"}}}}},
|
||||
)
|
||||
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
class MyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
async def async_step_user(self, user_input=None):
|
||||
return self.async_show_form(
|
||||
data_schema=vol.Schema({vol.Required("host"): str}),
|
||||
)
|
||||
""",
|
||||
"homeassistant.components.test_int.config_flow",
|
||||
)
|
||||
root_node.file = str(integration_dir / "config_flow.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(flow_translations_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_implicit_step_id_missing_field_flagged(
|
||||
linter: UnittestLinter,
|
||||
flow_translations_checker: ConfigFlowTranslationsChecker,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Test that missing field is flagged when step_id is inferred."""
|
||||
integration_dir = _make_integration(
|
||||
tmp_path,
|
||||
{"config": {"step": {"user": {"data": {"host": "Host"}}}}},
|
||||
)
|
||||
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
class MyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
async def async_step_user(self, user_input=None):
|
||||
return self.async_show_form(
|
||||
data_schema=vol.Schema({
|
||||
vol.Required("host"): str,
|
||||
vol.Required("missing"): str,
|
||||
}),
|
||||
)
|
||||
""",
|
||||
"homeassistant.components.test_int.config_flow",
|
||||
)
|
||||
root_node.file = str(integration_dir / "config_flow.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(flow_translations_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
assert messages[0].msg_id == "home-assistant-config-flow-field-not-translated"
|
||||
assert "missing" in messages[0].args[0]
|
||||
|
||||
|
||||
def test_section_fields_translated_ok(
|
||||
linter: UnittestLinter,
|
||||
flow_translations_checker: ConfigFlowTranslationsChecker,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Test that section fields are checked against sections translations."""
|
||||
integration_dir = _make_integration(
|
||||
tmp_path,
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {"host": "Host"},
|
||||
"sections": {
|
||||
"advanced": {
|
||||
"data": {"ssl": "Use SSL", "verify": "Verify"},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
class MyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
async def async_step_user(self, user_input=None):
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema({
|
||||
vol.Required("host"): str,
|
||||
vol.Required("advanced"): section(
|
||||
vol.Schema({
|
||||
vol.Required("ssl"): bool,
|
||||
vol.Required("verify"): bool,
|
||||
}),
|
||||
{"collapsed": True},
|
||||
),
|
||||
}),
|
||||
)
|
||||
""",
|
||||
"homeassistant.components.test_int.config_flow",
|
||||
)
|
||||
root_node.file = str(integration_dir / "config_flow.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(flow_translations_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_section_missing_field_flagged(
|
||||
linter: UnittestLinter,
|
||||
flow_translations_checker: ConfigFlowTranslationsChecker,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Test that missing section fields are flagged."""
|
||||
integration_dir = _make_integration(
|
||||
tmp_path,
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {"host": "Host"},
|
||||
"sections": {
|
||||
"advanced": {
|
||||
"data": {"ssl": "Use SSL"},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
class MyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
async def async_step_user(self, user_input=None):
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema({
|
||||
vol.Required("host"): str,
|
||||
vol.Required("advanced"): section(
|
||||
vol.Schema({
|
||||
vol.Required("ssl"): bool,
|
||||
vol.Required("missing_field"): bool,
|
||||
}),
|
||||
{"collapsed": True},
|
||||
),
|
||||
}),
|
||||
)
|
||||
""",
|
||||
"homeassistant.components.test_int.config_flow",
|
||||
)
|
||||
root_node.file = str(integration_dir / "config_flow.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(flow_translations_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
assert "missing_field" in messages[0].args[0]
|
||||
|
||||
|
||||
def test_section_attribute_form_ok(
|
||||
linter: UnittestLinter,
|
||||
flow_translations_checker: ConfigFlowTranslationsChecker,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Test that data_entry_flow.section(...) is recognized as a section."""
|
||||
integration_dir = _make_integration(
|
||||
tmp_path,
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {"host": "Host"},
|
||||
"sections": {
|
||||
"advanced": {
|
||||
"data": {"ssl": "Use SSL"},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
class MyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
async def async_step_user(self, user_input=None):
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema({
|
||||
vol.Required("host"): str,
|
||||
vol.Required("advanced"): data_entry_flow.section(
|
||||
vol.Schema({vol.Required("ssl"): bool}),
|
||||
{"collapsed": True},
|
||||
),
|
||||
}),
|
||||
)
|
||||
""",
|
||||
"homeassistant.components.test_int.config_flow",
|
||||
)
|
||||
root_node.file = str(integration_dir / "config_flow.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(flow_translations_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_constant_field_names_resolved(
|
||||
linter: UnittestLinter,
|
||||
flow_translations_checker: ConfigFlowTranslationsChecker,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Test that constant field names are resolved via inference."""
|
||||
integration_dir = _make_integration(
|
||||
tmp_path,
|
||||
{"config": {"step": {"user": {"data": {"host": "Host"}}}}},
|
||||
)
|
||||
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
CONF_HOST = "host"
|
||||
|
||||
class MyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
async def async_step_user(self, user_input=None):
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
|
||||
)
|
||||
""",
|
||||
"homeassistant.components.test_int.config_flow",
|
||||
)
|
||||
root_node.file = str(integration_dir / "config_flow.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(flow_translations_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_dict_unpacking_in_schema(
|
||||
linter: UnittestLinter,
|
||||
flow_translations_checker: ConfigFlowTranslationsChecker,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Test that **dict unpacking in schema dicts is resolved."""
|
||||
integration_dir = _make_integration(
|
||||
tmp_path,
|
||||
{"config": {"step": {"user": {"data": {"host": "Host", "port": "Port"}}}}},
|
||||
)
|
||||
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
BASE = {vol.Required("host"): str}
|
||||
|
||||
class MyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
async def async_step_user(self, user_input=None):
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema({**BASE, vol.Optional("port"): int}),
|
||||
)
|
||||
""",
|
||||
"homeassistant.components.test_int.config_flow",
|
||||
)
|
||||
root_node.file = str(integration_dir / "config_flow.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(flow_translations_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_schema_variable_resolved(
|
||||
linter: UnittestLinter,
|
||||
flow_translations_checker: ConfigFlowTranslationsChecker,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Test that schema passed via a variable is resolved."""
|
||||
integration_dir = _make_integration(
|
||||
tmp_path,
|
||||
{"config": {"step": {"user": {"data": {"host": "Host"}}}}},
|
||||
)
|
||||
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
USER_SCHEMA = vol.Schema({vol.Required("host"): str})
|
||||
|
||||
class MyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
async def async_step_user(self, user_input=None):
|
||||
return self.async_show_form(step_id="user", data_schema=USER_SCHEMA)
|
||||
""",
|
||||
"homeassistant.components.test_int.config_flow",
|
||||
)
|
||||
root_node.file = str(integration_dir / "config_flow.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(flow_translations_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
# --- Edge cases ---
|
||||
|
||||
|
||||
def test_no_strings_json_no_warning(
|
||||
linter: UnittestLinter,
|
||||
flow_translations_checker: ConfigFlowTranslationsChecker,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""No warning when strings.json doesn't exist."""
|
||||
integration_dir = _make_integration(tmp_path)
|
||||
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
class MyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
async def async_step_user(self, user_input=None):
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema({vol.Required("host"): str}),
|
||||
)
|
||||
""",
|
||||
"homeassistant.components.test_int.config_flow",
|
||||
)
|
||||
root_node.file = str(integration_dir / "config_flow.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(flow_translations_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_not_config_flow_module_ignored(
|
||||
linter: UnittestLinter,
|
||||
flow_translations_checker: ConfigFlowTranslationsChecker,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Non-config_flow modules are ignored."""
|
||||
integration_dir = _make_integration(
|
||||
tmp_path, {"config": {"step": {"user": {"data": {}}}}}
|
||||
)
|
||||
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
class MyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
async def async_step_user(self, user_input=None):
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema({vol.Required("host"): str}),
|
||||
)
|
||||
""",
|
||||
"homeassistant.components.test_int.sensor",
|
||||
)
|
||||
root_node.file = str(integration_dir / "sensor.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(flow_translations_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_no_data_schema_no_warning(
|
||||
linter: UnittestLinter,
|
||||
flow_translations_checker: ConfigFlowTranslationsChecker,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""No warning when async_show_form has no data_schema."""
|
||||
integration_dir = _make_integration(
|
||||
tmp_path, {"config": {"step": {"link": {"title": "Link"}}}}
|
||||
)
|
||||
|
||||
root_node = astroid.parse(
|
||||
"""
|
||||
class MyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
async def async_step_link(self, user_input=None):
|
||||
return self.async_show_form(step_id="link")
|
||||
""",
|
||||
"homeassistant.components.test_int.config_flow",
|
||||
)
|
||||
root_node.file = str(integration_dir / "config_flow.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(flow_translations_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walker.walk(root_node)
|
||||
Reference in New Issue
Block a user