Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 13 additions & 10 deletions homeassistant/components/netatmo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@
from homeassistant.const import CONF_WEBHOOK_ID, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import (
aiohttp_client,
config_entry_oauth2_flow,
config_validation as cv,
from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
OAuth2Session,
async_get_config_entry_implementation,
)
from homeassistant.helpers.device_registry import DeviceEntry
from homeassistant.helpers.dispatcher import async_dispatcher_send
Expand Down Expand Up @@ -73,17 +74,19 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:

async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Netatmo from a config entry."""
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
)
try:
implementation = await async_get_config_entry_implementation(hass, entry)
except ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err

# Set unique id if non was set (migration)
if not entry.unique_id:
hass.config_entries.async_update_entry(entry, unique_id=DOMAIN)

session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
session = OAuth2Session(hass, entry, implementation)
try:
await session.async_ensure_token_valid()
except aiohttp.ClientResponseError as ex:
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/netatmo/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,11 @@
}
}
},
"exceptions": {
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
}
},
"options": {
"step": {
"public_weather": {
Expand Down
6 changes: 6 additions & 0 deletions homeassistant/components/tesla_fleet/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
OAuth2Session,
async_get_config_entry_implementation,
)
Expand Down Expand Up @@ -61,6 +62,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) -

try:
implementation = await async_get_config_entry_implementation(hass, entry)
except ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
except ValueError as e:
# Remove invalid implementation from config entry then raise AuthFailed
hass.config_entries.async_update_entry(
Expand Down
3 changes: 3 additions & 0 deletions homeassistant/components/tesla_fleet/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,9 @@
"no_cable": {
"message": "Charge cable will lock automatically when connected"
},
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
},
"update_failed": {
"message": "{endpoint} data request failed: {message}"
}
Expand Down
10 changes: 9 additions & 1 deletion homeassistant/components/toon/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
Platform,
)
from homeassistant.core import CoreState, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
OAuth2Session,
async_get_config_entry_implementation,
)
Expand Down Expand Up @@ -86,7 +88,13 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Toon from a config entry."""
implementation = await async_get_config_entry_implementation(hass, entry)
try:
implementation = await async_get_config_entry_implementation(hass, entry)
except ImplementationUnavailableError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="oauth2_implementation_unavailable",
) from err
session = OAuth2Session(hass, entry, implementation)

coordinator = ToonDataUpdateCoordinator(hass, entry, session)
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/toon/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@
}
}
},
"exceptions": {
"oauth2_implementation_unavailable": {
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
}
},
"services": {
"update": {
"description": "Updates all entities with fresh data from Toon.",
Expand Down
24 changes: 24 additions & 0 deletions homeassistant/helpers/template/extensions/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from jinja2.parser import Parser

if TYPE_CHECKING:
from homeassistant.core import HomeAssistant
from homeassistant.helpers.template import TemplateEnvironment


Expand All @@ -26,6 +27,7 @@ class TemplateFunction:
limited_ok: bool = (
True # Whether this function is available in limited environments
)
requires_hass: bool = False # Whether this function requires hass to be available


class BaseTemplateExtension(Extension):
Expand All @@ -44,6 +46,10 @@ def __init__(

if functions:
for template_func in functions:
# Skip functions that require hass when hass is not available
if template_func.requires_hass and self.environment.hass is None:
continue

# Skip functions not allowed in limited environments
if self.environment.limited and not template_func.limited_ok:
continue
Expand All @@ -55,6 +61,24 @@ def __init__(
if template_func.as_test:
environment.tests[template_func.name] = template_func.func

@property
def hass(self) -> HomeAssistant:
"""Return the Home Assistant instance.

This property should only be used in extensions that have functions
marked with requires_hass=True, as it assumes hass is not None.

Raises:
RuntimeError: If hass is not available in the environment.
"""
if self.environment.hass is None:
raise RuntimeError(
"Home Assistant instance is not available. "
"This property should only be used in extensions with "
"functions marked requires_hass=True."
)
return self.environment.hass

def parse(self, parser: Parser) -> Node | list[Node]:
"""Required by Jinja2 Extension base class."""
return []
28 changes: 22 additions & 6 deletions tests/components/growatt_server/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ def mock_growatt_v1_api():
Methods mocked for switch and number operations:
- min_write_parameter: Called by switch/number entities to change settings
"""
with patch("growattServer.OpenApiV1", autospec=True) as mock_v1_api_class:
with patch(
"homeassistant.components.growatt_server.config_flow.growattServer.OpenApiV1",
autospec=True,
) as mock_v1_api_class:
mock_v1_api = mock_v1_api_class.return_value

# Called during setup to discover devices
Expand Down Expand Up @@ -119,7 +122,10 @@ def mock_growatt_classic_api():
Methods mocked for device-specific tests:
- tlx_detail: Provides TLX device data (kept for potential future tests)
"""
with patch("growattServer.GrowattApi", autospec=True) as mock_classic_api_class:
with patch(
"homeassistant.components.growatt_server.config_flow.growattServer.GrowattApi",
autospec=True,
) as mock_classic_api_class:
# Use the autospec'd mock instance instead of creating a new Mock()
mock_classic_api = mock_classic_api_class.return_value

Expand Down Expand Up @@ -167,10 +173,10 @@ def mock_config_entry() -> MockConfigEntry:
CONF_TOKEN: "test_token_123",
CONF_URL: DEFAULT_URL,
"user_id": "12345",
CONF_PLANT_ID: "plant_123",
CONF_PLANT_ID: "123456",
"name": "Test Plant",
},
unique_id="plant_123",
unique_id="123456",
)


Expand All @@ -188,10 +194,10 @@ def mock_config_entry_classic() -> MockConfigEntry:
CONF_USERNAME: "test_user",
CONF_PASSWORD: "test_password",
CONF_URL: DEFAULT_URL,
CONF_PLANT_ID: "12345",
CONF_PLANT_ID: "123456",
"name": "Test Plant",
},
unique_id="12345",
unique_id="123456",
)


Expand All @@ -215,3 +221,13 @@ async def init_integration(
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
return mock_config_entry


@pytest.fixture
def mock_setup_entry():
"""Mock async_setup_entry to prevent actual setup during config flow tests."""
with patch(
"homeassistant.components.growatt_server.async_setup_entry",
return_value=True,
) as mock:
yield mock
62 changes: 0 additions & 62 deletions tests/components/growatt_server/snapshots/test_init.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -61,65 +61,3 @@
'via_device_id': None,
})
# ---
# name: test_multiple_devices_discovered[device_min123456]
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': None,
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'growatt_server',
'MIN123456',
),
}),
'labels': set({
}),
'manufacturer': 'Growatt',
'model': None,
'model_id': None,
'name': 'MIN123456',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'sw_version': None,
'via_device_id': None,
})
# ---
# name: test_multiple_devices_discovered[device_min789012]
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': None,
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'growatt_server',
'MIN789012',
),
}),
'labels': set({
}),
'manufacturer': 'Growatt',
'model': None,
'model_id': None,
'name': 'MIN789012',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'sw_version': None,
'via_device_id': None,
})
# ---
31 changes: 0 additions & 31 deletions tests/components/growatt_server/snapshots/test_number.ambr
Original file line number Diff line number Diff line change
@@ -1,35 +1,4 @@
# serializer version: 1
# name: test_number_device_registry
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': None,
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': None,
'id': <ANY>,
'identifiers': set({
tuple(
'growatt_server',
'MIN123456',
),
}),
'labels': set({
}),
'manufacturer': 'Growatt',
'model': None,
'model_id': None,
'name': 'MIN123456',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'sw_version': None,
'via_device_id': None,
})
# ---
# name: test_number_entities[number.min123456_battery_charge_power_limit-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
Expand Down
Loading
Loading