Compare commits

..

7 Commits

Author SHA1 Message Date
J. Nick Koston
5f735b6042 Handle OAuth token request exceptions in Yale setup
Map OAuth token reauth failures to config entry auth failures and
treat other token request errors as retryable setup failures.

Closes #163466
2026-03-12 17:17:33 -10:00
J. Nick Koston
9d962d3815 Add missing ON_OFF support and target_temperature_step to ESPHome water heater (#165427)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-12 16:10:29 -10:00
Bram Kragten
786fd40ae8 Update frontend to 20260312.0 (#165420) 2026-03-12 23:07:04 +01:00
Joakim Plate
5ec65dbd58 Remove use of media player internals in arcam (#165359) 2026-03-12 21:55:39 +00:00
Josef Zweck
35878bb203 Bump onedrive-personal-sdk to 0.1.7 (#165401) 2026-03-12 21:59:40 +01:00
Arie Catsman
e14d88ff55 Bump pyenphase to 2.4.6 (#165402) 2026-03-12 20:06:49 +00:00
Erwin Douna
d04efbfe48 Add platinum badge to Portainer (#165048)
Co-authored-by: Ariel Ebersberger <31776703+justanotherariel@users.noreply.github.com>
2026-03-12 19:30:31 +01:00
24 changed files with 620 additions and 253 deletions

View File

@@ -8,7 +8,7 @@
"iot_class": "local_polling",
"loggers": ["pyenphase"],
"quality_scale": "platinum",
"requirements": ["pyenphase==2.4.5"],
"requirements": ["pyenphase==2.4.6"],
"zeroconf": [
{
"type": "_enphase-envoy._tcp.local."

View File

@@ -5,7 +5,13 @@ from __future__ import annotations
from functools import partial
from typing import Any
from aioesphomeapi import EntityInfo, WaterHeaterInfo, WaterHeaterMode, WaterHeaterState
from aioesphomeapi import (
EntityInfo,
WaterHeaterFeature,
WaterHeaterInfo,
WaterHeaterMode,
WaterHeaterState,
)
from homeassistant.components.water_heater import (
WaterHeaterEntity,
@@ -54,6 +60,7 @@ class EsphomeWaterHeater(
static_info = self._static_info
self._attr_min_temp = static_info.min_temperature
self._attr_max_temp = static_info.max_temperature
self._attr_target_temperature_step = static_info.target_temperature_step
features = WaterHeaterEntityFeature.TARGET_TEMPERATURE
if static_info.supported_modes:
features |= WaterHeaterEntityFeature.OPERATION_MODE
@@ -63,6 +70,8 @@ class EsphomeWaterHeater(
]
else:
self._attr_operation_list = None
if static_info.supported_features & WaterHeaterFeature.SUPPORTS_ON_OFF:
features |= WaterHeaterEntityFeature.ON_OFF
self._attr_supported_features = features
@property
@@ -101,6 +110,24 @@ class EsphomeWaterHeater(
device_id=self._static_info.device_id,
)
@convert_api_error_ha_error
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the water heater on."""
self._client.water_heater_command(
key=self._key,
on=True,
device_id=self._static_info.device_id,
)
@convert_api_error_ha_error
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the water heater off."""
self._client.water_heater_command(
key=self._key,
on=False,
device_id=self._static_info.device_id,
)
async_setup_entry = partial(
platform_async_setup_entry,

View File

@@ -21,5 +21,5 @@
"integration_type": "system",
"preview_features": { "winter_mode": {} },
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20260304.0"]
"requirements": ["home-assistant-frontend==20260312.0"]
}

View File

@@ -10,11 +10,7 @@ from functools import partial, wraps
import logging
from typing import Any, Concatenate
from aiohasupervisor import (
AddonNotSupportedError,
SupervisorError,
SupervisorNotFoundError,
)
from aiohasupervisor import SupervisorError
from aiohasupervisor.models import (
AddonsOptions,
AddonState as SupervisorAddonState,
@@ -169,7 +165,15 @@ class AddonManager:
)
addon_info = await self._supervisor_client.addons.addon_info(self.addon_slug)
return self._async_convert_installed_addon_info(addon_info)
addon_state = self.async_get_addon_state(addon_info)
return AddonInfo(
available=addon_info.available,
hostname=addon_info.hostname,
options=addon_info.options,
state=addon_state,
update_available=addon_info.update_available,
version=addon_info.version,
)
@callback
def async_get_addon_state(self, addon_info: InstalledAddonComplete) -> AddonState:
@@ -185,20 +189,6 @@ class AddonManager:
return addon_state
@callback
def _async_convert_installed_addon_info(
self, addon_info: InstalledAddonComplete
) -> AddonInfo:
"""Convert InstalledAddonComplete model to AddonInfo model."""
return AddonInfo(
available=addon_info.available,
hostname=addon_info.hostname,
options=addon_info.options,
state=self.async_get_addon_state(addon_info),
update_available=addon_info.update_available,
version=addon_info.version,
)
@api_error(
"Failed to set the {addon_name} app options",
expected_error_type=SupervisorError,
@@ -209,17 +199,21 @@ class AddonManager:
self.addon_slug, AddonsOptions(config=config)
)
def _check_addon_available(self, addon_info: AddonInfo) -> None:
"""Check if the managed add-on is available."""
if not addon_info.available:
raise AddonError(f"{self.addon_name} app is not available")
@api_error(
"Failed to install the {addon_name} app", expected_error_type=SupervisorError
)
async def async_install_addon(self) -> None:
"""Install the managed add-on."""
try:
await self._supervisor_client.store.install_addon(self.addon_slug)
except AddonNotSupportedError as err:
raise AddonError(
f"{self.addon_name} app is not available: {err!s}"
) from None
addon_info = await self.async_get_addon_info()
self._check_addon_available(addon_info)
await self._supervisor_client.store.install_addon(self.addon_slug)
@api_error(
"Failed to uninstall the {addon_name} app",
@@ -232,29 +226,17 @@ class AddonManager:
@api_error("Failed to update the {addon_name} app")
async def async_update_addon(self) -> None:
"""Update the managed add-on if needed."""
try:
# Not using async_get_addon_info here because it would make an unnecessary
# call to /store/addon/{slug}/info. This will raise if the addon is not
# installed so one call to /addon/{slug}/info is all that is needed
addon_info = await self._supervisor_client.addons.addon_info(
self.addon_slug
)
except SupervisorNotFoundError:
raise AddonError(f"{self.addon_name} app is not installed") from None
addon_info = await self.async_get_addon_info()
self._check_addon_available(addon_info)
if addon_info.state is AddonState.NOT_INSTALLED:
raise AddonError(f"{self.addon_name} app is not installed")
if not addon_info.update_available:
return
try:
await self._supervisor_client.store.addon_availability(self.addon_slug)
except AddonNotSupportedError as err:
raise AddonError(
f"{self.addon_name} app is not available: {err!s}"
) from None
await self.async_create_backup(
addon_info=self._async_convert_installed_addon_info(addon_info)
)
await self.async_create_backup()
await self._supervisor_client.store.update_addon(
self.addon_slug, StoreAddonUpdate(backup=False)
)
@@ -284,14 +266,10 @@ class AddonManager:
"Failed to create a backup of the {addon_name} app",
expected_error_type=SupervisorError,
)
async def async_create_backup(self, *, addon_info: AddonInfo | None = None) -> None:
async def async_create_backup(self) -> None:
"""Create a partial backup of the managed add-on."""
if addon_info:
addon_version = addon_info.version
else:
addon_version = (await self.async_get_addon_info()).version
name = f"addon_{self.addon_slug}_{addon_version}"
addon_info = await self.async_get_addon_info()
name = f"addon_{self.addon_slug}_{addon_info.version}"
self._logger.debug("Creating backup: %s", name)
await self._supervisor_client.backups.partial_backup(

View File

@@ -10,5 +10,5 @@
"iot_class": "cloud_polling",
"loggers": ["onedrive_personal_sdk"],
"quality_scale": "platinum",
"requirements": ["onedrive-personal-sdk==0.1.6"]
"requirements": ["onedrive-personal-sdk==0.1.7"]
}

View File

@@ -10,5 +10,5 @@
"iot_class": "cloud_polling",
"loggers": ["onedrive_personal_sdk"],
"quality_scale": "platinum",
"requirements": ["onedrive-personal-sdk==0.1.6"]
"requirements": ["onedrive-personal-sdk==0.1.7"]
}

View File

@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/portainer",
"integration_type": "service",
"iot_class": "local_polling",
"quality_scale": "bronze",
"quality_scale": "platinum",
"requirements": ["pyportainer==1.0.33"]
}

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
from pathlib import Path
from typing import cast
from aiohttp import ClientResponseError
from aiohttp import ClientError
from yalexs.const import Brand
from yalexs.exceptions import YaleApiError
from yalexs.manager.const import CONF_BRAND
@@ -15,7 +15,12 @@ from yalexs.manager.gateway import Config as YaleXSConfig
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
OAuth2TokenRequestError,
OAuth2TokenRequestReauthError,
)
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
@@ -42,11 +47,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: YaleConfigEntry) -> bool
yale_gateway = YaleGateway(Path(hass.config.config_dir), session, oauth_session)
try:
await async_setup_yale(hass, entry, yale_gateway)
except OAuth2TokenRequestReauthError as err:
raise ConfigEntryAuthFailed from err
except (RequireValidation, InvalidAuth) as err:
raise ConfigEntryAuthFailed from err
except TimeoutError as err:
raise ConfigEntryNotReady("Timed out connecting to yale api") from err
except (YaleApiError, ClientResponseError, CannotConnect) as err:
except (
YaleApiError,
OAuth2TokenRequestError,
ClientError,
CannotConnect,
) as err:
raise ConfigEntryNotReady from err
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True

View File

@@ -40,7 +40,7 @@ habluetooth==5.9.1
hass-nabucasa==2.0.0
hassil==3.5.0
home-assistant-bluetooth==1.13.1
home-assistant-frontend==20260304.0
home-assistant-frontend==20260312.0
home-assistant-intents==2026.3.3
httpx==0.28.1
ifaddr==0.2.0

6
requirements_all.txt generated
View File

@@ -1223,7 +1223,7 @@ hole==0.9.0
holidays==0.84
# homeassistant.components.frontend
home-assistant-frontend==20260304.0
home-assistant-frontend==20260312.0
# homeassistant.components.conversation
home-assistant-intents==2026.3.3
@@ -1676,7 +1676,7 @@ ondilo==0.5.0
# homeassistant.components.onedrive
# homeassistant.components.onedrive_for_business
onedrive-personal-sdk==0.1.6
onedrive-personal-sdk==0.1.7
# homeassistant.components.onvif
onvif-zeep-async==4.0.4
@@ -2071,7 +2071,7 @@ pyegps==0.2.5
pyemoncms==0.1.3
# homeassistant.components.enphase_envoy
pyenphase==2.4.5
pyenphase==2.4.6
# homeassistant.components.envisalink
pyenvisalink==4.7

View File

@@ -1084,7 +1084,7 @@ hole==0.9.0
holidays==0.84
# homeassistant.components.frontend
home-assistant-frontend==20260304.0
home-assistant-frontend==20260312.0
# homeassistant.components.conversation
home-assistant-intents==2026.3.3
@@ -1462,7 +1462,7 @@ ondilo==0.5.0
# homeassistant.components.onedrive
# homeassistant.components.onedrive_for_business
onedrive-personal-sdk==0.1.6
onedrive-personal-sdk==0.1.7
# homeassistant.components.onvif
onvif-zeep-async==4.0.4
@@ -1775,7 +1775,7 @@ pyegps==0.2.5
pyemoncms==0.1.3
# homeassistant.components.enphase_envoy
pyenphase==2.4.5
pyenphase==2.4.6
# homeassistant.components.everlights
pyeverlights==0.1.0

View File

@@ -44,12 +44,14 @@ def state_1_fixture(client: Mock) -> State:
state.zn = 1
state.get_power.return_value = True
state.get_volume.return_value = 0.0
state.get_source.return_value = None
state.get_source_list.return_value = []
state.get_incoming_audio_format.return_value = (None, None)
state.get_incoming_video_parameters.return_value = None
state.get_incoming_audio_sample_rate.return_value = 0
state.get_mute.return_value = None
state.get_decode_modes.return_value = []
state.get_decode_mode.return_value = None
return state
@@ -61,12 +63,14 @@ def state_2_fixture(client: Mock) -> State:
state.zn = 2
state.get_power.return_value = True
state.get_volume.return_value = 0.0
state.get_source.return_value = None
state.get_source_list.return_value = []
state.get_incoming_audio_format.return_value = (None, None)
state.get_incoming_video_parameters.return_value = None
state.get_incoming_audio_sample_rate.return_value = 0
state.get_mute.return_value = None
state.get_decode_modes.return_value = []
state.get_decode_mode.return_value = None
return state
@@ -90,7 +94,7 @@ async def player_setup_fixture(
state_1: State,
state_2: State,
client: Mock,
) -> AsyncGenerator[str]:
) -> AsyncGenerator[None]:
"""Get standard player."""
def state_mock(cli, zone):
@@ -101,7 +105,15 @@ async def player_setup_fixture(
raise ValueError(f"Unknown player zone: {zone}")
async def _mock_run_client(hass: HomeAssistant, runtime_data, interval):
for coordinator in runtime_data.coordinators.values():
coordinators = runtime_data.coordinators
def _notify_data_updated() -> None:
for coordinator in coordinators.values():
coordinator.async_notify_data_updated()
client.notify_data_updated = _notify_data_updated
for coordinator in coordinators.values():
coordinator.async_notify_connected()
await async_setup_component(hass, "homeassistant", {})
@@ -119,4 +131,4 @@ async def player_setup_fixture(
):
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
yield MOCK_ENTITY_ID
yield

View File

@@ -0,0 +1,105 @@
# serializer version: 1
# name: test_setup[media_player.arcam_fmj_127_0_0_1_zone_1-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'media_player',
'entity_category': None,
'entity_id': 'media_player.arcam_fmj_127_0_0_1_zone_1',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Zone 1',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Zone 1',
'platform': 'arcam_fmj',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <MediaPlayerEntityFeature: 200588>,
'translation_key': None,
'unique_id': '456789abcdef-1',
'unit_of_measurement': None,
})
# ---
# name: test_setup[media_player.arcam_fmj_127_0_0_1_zone_1-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Arcam FMJ (127.0.0.1) Zone 1',
'supported_features': <MediaPlayerEntityFeature: 200588>,
'volume_level': 0.0,
}),
'context': <ANY>,
'entity_id': 'media_player.arcam_fmj_127_0_0_1_zone_1',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_setup[media_player.arcam_fmj_127_0_0_1_zone_2_zone_2-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'media_player',
'entity_category': None,
'entity_id': 'media_player.arcam_fmj_127_0_0_1_zone_2_zone_2',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'object_id_base': 'Zone 2',
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Zone 2',
'platform': 'arcam_fmj',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <MediaPlayerEntityFeature: 135052>,
'translation_key': None,
'unique_id': '456789abcdef-2',
'unit_of_measurement': None,
})
# ---
# name: test_setup[media_player.arcam_fmj_127_0_0_1_zone_2_zone_2-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Arcam FMJ (127.0.0.1) Zone 2 Zone 2',
'supported_features': <MediaPlayerEntityFeature: 135052>,
'volume_level': 0.0,
}),
'context': <ANY>,
'entity_id': 'media_player.arcam_fmj_127_0_0_1_zone_2_zone_2',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---

View File

@@ -9,6 +9,8 @@ from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.setup import async_setup_component
from .conftest import MOCK_ENTITY_ID
from tests.common import MockConfigEntry, async_get_device_automations
@@ -59,7 +61,7 @@ async def test_if_fires_on_turn_on_request(
state_1: State,
) -> None:
"""Test for turn_on and turn_off triggers firing."""
entry = entity_registry.async_get(player_setup)
entry = entity_registry.async_get(MOCK_ENTITY_ID)
state_1.get_power.return_value = None
@@ -91,13 +93,13 @@ async def test_if_fires_on_turn_on_request(
await hass.services.async_call(
"media_player",
"turn_on",
{"entity_id": player_setup},
{"entity_id": MOCK_ENTITY_ID},
blocking=True,
)
await hass.async_block_till_done()
assert len(service_calls) == 2
assert service_calls[1].data["some"] == player_setup
assert service_calls[1].data["some"] == MOCK_ENTITY_ID
assert service_calls[1].data["id"] == 0
@@ -109,7 +111,7 @@ async def test_if_fires_on_turn_on_request_legacy(
state_1: State,
) -> None:
"""Test for turn_on and turn_off triggers firing."""
entry = entity_registry.async_get(player_setup)
entry = entity_registry.async_get(MOCK_ENTITY_ID)
state_1.get_power.return_value = None
@@ -141,11 +143,11 @@ async def test_if_fires_on_turn_on_request_legacy(
await hass.services.async_call(
"media_player",
"turn_on",
{"entity_id": player_setup},
{"entity_id": MOCK_ENTITY_ID},
blocking=True,
)
await hass.async_block_till_done()
assert len(service_calls) == 2
assert service_calls[1].data["some"] == player_setup
assert service_calls[1].data["some"] == MOCK_ENTITY_ID
assert service_calls[1].data["id"] == 0

View File

@@ -6,6 +6,7 @@ from unittest.mock import Mock, PropertyMock, patch
from arcam.fmj import ConnectionFailed, DecodeMode2CH, DecodeModeMCH, SourceCodes
from arcam.fmj.state import State
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.arcam_fmj.media_player import ArcamFmj
from homeassistant.components.homeassistant import (
@@ -14,145 +15,146 @@ from homeassistant.components.homeassistant import (
)
from homeassistant.components.media_player import (
ATTR_INPUT_SOURCE,
ATTR_MEDIA_ARTIST,
ATTR_MEDIA_CHANNEL,
ATTR_MEDIA_CONTENT_TYPE,
ATTR_MEDIA_VOLUME_LEVEL,
ATTR_MEDIA_VOLUME_MUTED,
ATTR_SOUND_MODE,
ATTR_SOUND_MODE_LIST,
DATA_COMPONENT,
DOMAIN as MEDIA_PLAYER_DOMAIN,
SERVICE_SELECT_SOUND_MODE,
SERVICE_SELECT_SOURCE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
SERVICE_VOLUME_DOWN,
SERVICE_VOLUME_MUTE,
SERVICE_VOLUME_SET,
SERVICE_VOLUME_UP,
MediaType,
)
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_IDENTIFIERS,
ATTR_MANUFACTURER,
ATTR_MODEL,
ATTR_NAME,
)
from homeassistant.core import HomeAssistant
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant, State as CoreState
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from .conftest import MOCK_ENTITY_ID, MOCK_HOST, MOCK_UUID
from .conftest import MOCK_ENTITY_ID
from tests.common import MockConfigEntry
MOCK_TURN_ON = {
"service": "switch.turn_on",
"data": {"entity_id": "switch.test"},
}
from tests.common import MockConfigEntry, snapshot_platform
@pytest.fixture(name="player")
def player_fixture(
@pytest.fixture(autouse=True)
def platform_fixture():
"""Only test single platform."""
with patch("homeassistant.components.arcam_fmj.PLATFORMS", [Platform.MEDIA_PLAYER]):
yield
@pytest.mark.usefixtures("player_setup")
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_setup(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_config_entry: MockConfigEntry,
client: Mock,
state_1: State,
player_setup: str,
) -> ArcamFmj:
"""Get standard player.
This fixture tests internals and should not be used going forward.
"""
player: ArcamFmj = hass.data[DATA_COMPONENT].get_entity(MOCK_ENTITY_ID)
player.async_write_ha_state = Mock(wraps=player.async_write_ha_state)
return player
async def update(player: ArcamFmj, force_refresh=False):
"""Force a update of player and return current state data."""
await player.async_update_ha_state(force_refresh=force_refresh)
return player.hass.states.get(player.entity_id)
async def test_properties(player: ArcamFmj) -> None:
"""Test standard properties."""
assert player.unique_id == f"{MOCK_UUID}-1"
assert player.device_info == {
ATTR_NAME: f"Arcam FMJ ({MOCK_HOST})",
ATTR_IDENTIFIERS: {
("arcam_fmj", MOCK_UUID),
},
ATTR_MODEL: "Arcam FMJ AVR",
ATTR_MANUFACTURER: "Arcam",
}
assert not player.should_poll
async def test_powered_off(
hass: HomeAssistant, player: ArcamFmj, state_1: State
entity_registry: er.EntityRegistry,
) -> None:
"""Test setup creates expected entities."""
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
async def update(hass: HomeAssistant, client: Mock, entity_id: str) -> CoreState:
"""Force a update of player and return current state data."""
client.notify_data_updated()
await hass.async_block_till_done()
data = hass.states.get(entity_id)
assert data
return data
@pytest.mark.usefixtures("player_setup")
async def test_powered_off(hass: HomeAssistant, client: Mock, state_1: State) -> None:
"""Test properties in powered off state."""
state_1.get_source.return_value = None
state_1.get_power.return_value = None
data = await update(player)
data = await update(hass, client, MOCK_ENTITY_ID)
assert "source" not in data.attributes
assert data.state == "off"
async def test_powered_on(player: ArcamFmj, state_1: State) -> None:
@pytest.mark.usefixtures("player_setup")
async def test_powered_on(hass: HomeAssistant, client: Mock, state_1: State) -> None:
"""Test properties in powered on state."""
state_1.get_source.return_value = SourceCodes.PVR
state_1.get_power.return_value = True
data = await update(player)
data = await update(hass, client, MOCK_ENTITY_ID)
assert data.attributes["source"] == "PVR"
assert data.state == "on"
async def test_supported_features(player: ArcamFmj) -> None:
"""Test supported features."""
data = await update(player)
assert data.attributes["supported_features"] == 200588
async def test_turn_on(player: ArcamFmj, state_1: State) -> None:
@pytest.mark.usefixtures("player_setup")
async def test_turn_on(hass: HomeAssistant, state_1: State) -> None:
"""Test turn on service."""
state_1.get_power.return_value = None
await player.async_turn_on()
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_TURN_ON,
service_data={ATTR_ENTITY_ID: MOCK_ENTITY_ID},
blocking=True,
)
state_1.set_power.assert_not_called()
state_1.get_power.return_value = False
await player.async_turn_on()
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_TURN_ON,
service_data={ATTR_ENTITY_ID: MOCK_ENTITY_ID},
blocking=True,
)
state_1.set_power.assert_called_with(True)
async def test_turn_off(player: ArcamFmj, state_1: State) -> None:
@pytest.mark.usefixtures("player_setup")
async def test_turn_off(hass: HomeAssistant, state_1: State) -> None:
"""Test command to turn off."""
await player.async_turn_off()
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_TURN_OFF,
service_data={ATTR_ENTITY_ID: MOCK_ENTITY_ID},
blocking=True,
)
state_1.set_power.assert_called_with(False)
@pytest.mark.parametrize("mute", [True, False])
async def test_mute_volume(player: ArcamFmj, state_1: State, mute: bool) -> None:
@pytest.mark.usefixtures("player_setup")
async def test_mute_volume(hass: HomeAssistant, state_1: State, mute: bool) -> None:
"""Test mute functionality."""
player.async_write_ha_state.reset_mock()
await player.async_mute_volume(mute)
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_VOLUME_MUTE,
service_data={ATTR_ENTITY_ID: MOCK_ENTITY_ID, ATTR_MEDIA_VOLUME_MUTED: mute},
blocking=True,
)
state_1.set_mute.assert_called_with(mute)
player.async_write_ha_state.assert_called_with()
async def test_name(player: ArcamFmj) -> None:
"""Test name."""
data = await update(player)
assert data.attributes["friendly_name"] == "Arcam FMJ (127.0.0.1) Zone 1"
async def test_update(hass: HomeAssistant, player_setup: str, state_1: State) -> None:
@pytest.mark.usefixtures("player_setup")
async def test_update(hass: HomeAssistant, state_1: State) -> None:
"""Test update."""
await hass.services.async_call(
HA_DOMAIN,
SERVICE_UPDATE_ENTITY,
service_data={ATTR_ENTITY_ID: player_setup},
service_data={ATTR_ENTITY_ID: MOCK_ENTITY_ID},
blocking=True,
)
state_1.update.assert_called_with()
@pytest.mark.usefixtures("player_setup")
async def test_update_lost(
hass: HomeAssistant,
player_setup: str,
state_1: State,
caplog: pytest.LogCaptureFixture,
) -> None:
@@ -162,7 +164,7 @@ async def test_update_lost(
await hass.services.async_call(
HA_DOMAIN,
SERVICE_UPDATE_ENTITY,
service_data={ATTR_ENTITY_ID: player_setup},
service_data={ATTR_ENTITY_ID: MOCK_ENTITY_ID},
blocking=True,
)
state_1.update.assert_called_with()
@@ -172,9 +174,9 @@ async def test_update_lost(
("source", "value"),
[("PVR", SourceCodes.PVR), ("BD", SourceCodes.BD), ("INVALID", None)],
)
@pytest.mark.usefixtures("player_setup")
async def test_select_source(
hass: HomeAssistant,
player_setup,
state_1: State,
source: str,
value: SourceCodes | None,
@@ -183,7 +185,7 @@ async def test_select_source(
await hass.services.async_call(
"media_player",
SERVICE_SELECT_SOURCE,
service_data={ATTR_ENTITY_ID: player_setup, ATTR_INPUT_SOURCE: source},
service_data={ATTR_ENTITY_ID: MOCK_ENTITY_ID, ATTR_INPUT_SOURCE: source},
blocking=True,
)
@@ -193,10 +195,11 @@ async def test_select_source(
state_1.set_source.assert_not_called()
async def test_source_list(player: ArcamFmj, state_1: State) -> None:
@pytest.mark.usefixtures("player_setup")
async def test_source_list(hass: HomeAssistant, client: Mock, state_1: State) -> None:
"""Test source list."""
state_1.get_source_list.return_value = [SourceCodes.BD]
data = await update(player)
data = await update(hass, client, MOCK_ENTITY_ID)
assert data.attributes["source_list"] == ["BD"]
@@ -207,26 +210,42 @@ async def test_source_list(player: ArcamFmj, state_1: State) -> None:
"DOLBY_PL",
],
)
async def test_select_sound_mode(player: ArcamFmj, state_1: State, mode: str) -> None:
@pytest.mark.usefixtures("player_setup")
async def test_select_sound_mode(
hass: HomeAssistant, state_1: State, mode: str
) -> None:
"""Test selection sound mode."""
await player.async_select_sound_mode(mode)
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_SELECT_SOUND_MODE,
service_data={ATTR_ENTITY_ID: MOCK_ENTITY_ID, ATTR_SOUND_MODE: mode},
blocking=True,
)
state_1.set_decode_mode.assert_called_with(mode)
async def test_volume_up(player: ArcamFmj, state_1: State) -> None:
@pytest.mark.usefixtures("player_setup")
async def test_volume_up(hass: HomeAssistant, state_1: State) -> None:
"""Test mute functionality."""
player.async_write_ha_state.reset_mock()
await player.async_volume_up()
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_VOLUME_UP,
service_data={ATTR_ENTITY_ID: MOCK_ENTITY_ID},
blocking=True,
)
state_1.inc_volume.assert_called_with()
player.async_write_ha_state.assert_called_with()
async def test_volume_down(player: ArcamFmj, state_1: State) -> None:
@pytest.mark.usefixtures("player_setup")
async def test_volume_down(hass: HomeAssistant, state_1: State) -> None:
"""Test mute functionality."""
player.async_write_ha_state.reset_mock()
await player.async_volume_down()
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_VOLUME_DOWN,
service_data={ATTR_ENTITY_ID: MOCK_ENTITY_ID},
blocking=True,
)
state_1.dec_volume.assert_called_with()
player.async_write_ha_state.assert_called_with()
@pytest.mark.parametrize(
@@ -237,10 +256,13 @@ async def test_volume_down(player: ArcamFmj, state_1: State) -> None:
(None, None),
],
)
async def test_sound_mode(player: ArcamFmj, state_1: State, mode, mode_enum) -> None:
@pytest.mark.usefixtures("player_setup")
async def test_sound_mode(
hass: HomeAssistant, client: Mock, state_1: State, mode, mode_enum
) -> None:
"""Test selection sound mode."""
state_1.get_decode_mode.return_value = mode_enum
data = await update(player)
data = await update(hass, client, MOCK_ENTITY_ID)
assert data.attributes.get(ATTR_SOUND_MODE) == mode
@@ -252,56 +274,73 @@ async def test_sound_mode(player: ArcamFmj, state_1: State, mode, mode_enum) ->
(None, None),
],
)
@pytest.mark.usefixtures("player_setup")
async def test_sound_mode_list(
player: ArcamFmj, state_1: State, modes, modes_enum
hass: HomeAssistant, client: Mock, state_1: State, modes, modes_enum
) -> None:
"""Test sound mode list."""
state_1.get_decode_modes.return_value = modes_enum
data = await update(player)
data = await update(hass, client, MOCK_ENTITY_ID)
assert data.attributes.get(ATTR_SOUND_MODE_LIST) == modes
async def test_is_volume_muted(player: ArcamFmj, state_1: State) -> None:
@pytest.mark.usefixtures("player_setup")
async def test_is_volume_muted(
hass: HomeAssistant, client: Mock, state_1: State
) -> None:
"""Test muted."""
state_1.get_mute.return_value = True
assert player.is_volume_muted is True
data = await update(hass, client, MOCK_ENTITY_ID)
assert data.attributes.get(ATTR_MEDIA_VOLUME_MUTED) is True
state_1.get_mute.return_value = False
assert player.is_volume_muted is False
data = await update(hass, client, MOCK_ENTITY_ID)
assert data.attributes.get(ATTR_MEDIA_VOLUME_MUTED) is False
state_1.get_mute.return_value = None
assert player.is_volume_muted is None
data = await update(hass, client, MOCK_ENTITY_ID)
assert data.attributes.get(ATTR_MEDIA_VOLUME_MUTED) is None
async def test_volume_level(player: ArcamFmj, state_1: State) -> None:
@pytest.mark.usefixtures("player_setup")
async def test_volume_level(hass: HomeAssistant, client: Mock, state_1: State) -> None:
"""Test volume."""
state_1.get_volume.return_value = 0
assert isclose(player.volume_level, 0.0)
data = await update(hass, client, MOCK_ENTITY_ID)
assert isclose(data.attributes[ATTR_MEDIA_VOLUME_LEVEL], 0.0)
state_1.get_volume.return_value = 50
assert isclose(player.volume_level, 50.0 / 99)
data = await update(hass, client, MOCK_ENTITY_ID)
assert isclose(data.attributes[ATTR_MEDIA_VOLUME_LEVEL], 50.0 / 99)
state_1.get_volume.return_value = 99
assert isclose(player.volume_level, 1.0)
data = await update(hass, client, MOCK_ENTITY_ID)
assert isclose(data.attributes[ATTR_MEDIA_VOLUME_LEVEL], 1.0)
state_1.get_volume.return_value = None
assert player.volume_level is None
data = await update(hass, client, MOCK_ENTITY_ID)
assert ATTR_MEDIA_VOLUME_LEVEL not in data.attributes
@pytest.mark.parametrize(("volume", "call"), [(0.0, 0), (0.5, 50), (1.0, 99)])
@pytest.mark.usefixtures("player_setup")
async def test_set_volume_level(
hass: HomeAssistant, player_setup: str, state_1: State, volume, call
hass: HomeAssistant, state_1: State, volume, call
) -> None:
"""Test setting volume."""
await hass.services.async_call(
"media_player",
SERVICE_VOLUME_SET,
service_data={ATTR_ENTITY_ID: player_setup, ATTR_MEDIA_VOLUME_LEVEL: volume},
service_data={ATTR_ENTITY_ID: MOCK_ENTITY_ID, ATTR_MEDIA_VOLUME_LEVEL: volume},
blocking=True,
)
state_1.set_volume.assert_called_with(call)
async def test_set_volume_level_lost(
hass: HomeAssistant, player_setup: str, state_1: State
) -> None:
@pytest.mark.usefixtures("player_setup")
async def test_set_volume_level_lost(hass: HomeAssistant, state_1: State) -> None:
"""Test setting volume, with a lost connection."""
state_1.set_volume.side_effect = ConnectionFailed()
@@ -310,7 +349,7 @@ async def test_set_volume_level_lost(
await hass.services.async_call(
"media_player",
SERVICE_VOLUME_SET,
service_data={ATTR_ENTITY_ID: player_setup, ATTR_MEDIA_VOLUME_LEVEL: 0.0},
service_data={ATTR_ENTITY_ID: MOCK_ENTITY_ID, ATTR_MEDIA_VOLUME_LEVEL: 0.0},
blocking=True,
)
@@ -324,12 +363,14 @@ async def test_set_volume_level_lost(
(None, None),
],
)
@pytest.mark.usefixtures("player_setup")
async def test_media_content_type(
player: ArcamFmj, state_1: State, source, media_content_type
hass: HomeAssistant, client: Mock, state_1: State, source, media_content_type
) -> None:
"""Test content type deduction."""
state_1.get_source.return_value = source
assert player.media_content_type == media_content_type
data = await update(hass, client, MOCK_ENTITY_ID)
assert data.attributes.get(ATTR_MEDIA_CONTENT_TYPE) == media_content_type
@pytest.mark.parametrize(
@@ -342,14 +383,16 @@ async def test_media_content_type(
(SourceCodes.PVR, "dab", "rds", None),
],
)
@pytest.mark.usefixtures("player_setup")
async def test_media_channel(
player: ArcamFmj, state_1: State, source, dab, rds, channel
hass: HomeAssistant, client: Mock, state_1: State, source, dab, rds, channel
) -> None:
"""Test media channel."""
state_1.get_dab_station.return_value = dab
state_1.get_rds_information.return_value = rds
state_1.get_source.return_value = source
assert player.media_channel == channel
data = await update(hass, client, MOCK_ENTITY_ID)
assert data.attributes.get(ATTR_MEDIA_CHANNEL) == channel
@pytest.mark.parametrize(
@@ -360,13 +403,15 @@ async def test_media_channel(
(SourceCodes.DAB, None, None),
],
)
@pytest.mark.usefixtures("player_setup")
async def test_media_artist(
player: ArcamFmj, state_1: State, source, dls, artist
hass: HomeAssistant, client: Mock, state_1: State, source, dls, artist
) -> None:
"""Test media artist."""
state_1.get_dls_pdt.return_value = dls
state_1.get_source.return_value = source
assert player.media_artist == artist
data = await update(hass, client, MOCK_ENTITY_ID)
assert data.attributes.get(ATTR_MEDIA_ARTIST) == artist
@pytest.mark.parametrize(
@@ -377,8 +422,9 @@ async def test_media_artist(
(None, None, None),
],
)
@pytest.mark.usefixtures("player_setup")
async def test_media_title(
player: ArcamFmj, state_1: State, source, channel, title
hass: HomeAssistant, client: Mock, state_1: State, source, channel, title
) -> None:
"""Test media title."""
@@ -387,7 +433,7 @@ async def test_media_title(
ArcamFmj, "media_channel", new_callable=PropertyMock
) as media_channel:
media_channel.return_value = channel
data = await update(player)
data = await update(hass, client, MOCK_ENTITY_ID)
if title is None:
assert "media_title" not in data.attributes
else:

View File

@@ -13,7 +13,6 @@ import string
from typing import TYPE_CHECKING, Any
from unittest.mock import AsyncMock, MagicMock, patch
from aiohasupervisor import SupervisorNotFoundError
from aiohasupervisor.models import (
Discovery,
GreenInfo,
@@ -314,7 +313,6 @@ def addon_not_installed_fixture(
"""Mock add-on not installed."""
from .hassio.common import mock_addon_not_installed # noqa: PLC0415
addon_info.side_effect = SupervisorNotFoundError
return mock_addon_not_installed(addon_store_info, addon_info)

View File

@@ -2,13 +2,22 @@
from unittest.mock import call
from aioesphomeapi import APIClient, WaterHeaterInfo, WaterHeaterMode, WaterHeaterState
from aioesphomeapi import (
APIClient,
WaterHeaterFeature,
WaterHeaterInfo,
WaterHeaterMode,
WaterHeaterState,
)
from homeassistant.components.water_heater import (
ATTR_OPERATION_LIST,
DOMAIN as WATER_HEATER_DOMAIN,
SERVICE_SET_OPERATION_MODE,
SERVICE_SET_TEMPERATURE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
WaterHeaterEntityFeature,
)
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE
from homeassistant.core import HomeAssistant
@@ -183,3 +192,130 @@ async def test_water_heater_set_operation_mode(
mock_client.water_heater_command.assert_has_calls(
[call(key=1, mode=WaterHeaterMode.GAS, device_id=0)]
)
async def test_water_heater_on_off(
hass: HomeAssistant,
mock_client: APIClient,
mock_generic_device_entry: MockGenericDeviceEntryType,
) -> None:
"""Test turning the water heater on and off."""
entity_info = [
WaterHeaterInfo(
object_id="my_boiler",
key=1,
name="My Boiler",
min_temperature=10.0,
max_temperature=85.0,
supported_features=WaterHeaterFeature.SUPPORTS_ON_OFF,
)
]
states = [
WaterHeaterState(
key=1,
target_temperature=50.0,
)
]
await mock_generic_device_entry(
mock_client=mock_client,
entity_info=entity_info,
states=states,
)
state = hass.states.get("water_heater.test_my_boiler")
assert state is not None
assert state.attributes["supported_features"] & WaterHeaterEntityFeature.ON_OFF
await hass.services.async_call(
WATER_HEATER_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: "water_heater.test_my_boiler"},
blocking=True,
)
mock_client.water_heater_command.assert_has_calls(
[call(key=1, on=True, device_id=0)]
)
mock_client.water_heater_command.reset_mock()
await hass.services.async_call(
WATER_HEATER_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: "water_heater.test_my_boiler"},
blocking=True,
)
mock_client.water_heater_command.assert_has_calls(
[call(key=1, on=False, device_id=0)]
)
async def test_water_heater_target_temperature_step(
hass: HomeAssistant,
mock_client: APIClient,
mock_generic_device_entry: MockGenericDeviceEntryType,
) -> None:
"""Test target temperature step is respected."""
entity_info = [
WaterHeaterInfo(
object_id="my_boiler",
key=1,
name="My Boiler",
min_temperature=10.0,
max_temperature=85.0,
target_temperature_step=5.0,
)
]
states = [
WaterHeaterState(
key=1,
target_temperature=50.0,
)
]
await mock_generic_device_entry(
mock_client=mock_client,
entity_info=entity_info,
states=states,
)
state = hass.states.get("water_heater.test_my_boiler")
assert state is not None
assert state.attributes["target_temp_step"] == 5.0
async def test_water_heater_no_on_off_without_feature(
hass: HomeAssistant,
mock_client: APIClient,
mock_generic_device_entry: MockGenericDeviceEntryType,
) -> None:
"""Test ON_OFF feature is not set when not supported."""
entity_info = [
WaterHeaterInfo(
object_id="my_boiler",
key=1,
name="My Boiler",
min_temperature=10.0,
max_temperature=85.0,
)
]
states = [
WaterHeaterState(
key=1,
target_temperature=50.0,
)
]
await mock_generic_device_entry(
mock_client=mock_client,
entity_info=entity_info,
states=states,
)
state = hass.states.get("water_heater.test_my_boiler")
assert state is not None
assert not (
state.attributes["supported_features"] & WaterHeaterEntityFeature.ON_OFF
)

View File

@@ -3,15 +3,11 @@
from __future__ import annotations
import asyncio
from typing import Any
from unittest.mock import AsyncMock, call
from uuid import uuid4
from aiohasupervisor import (
AddonNotSupportedArchitectureError,
AddonNotSupportedHomeAssistantVersionError,
AddonNotSupportedMachineTypeError,
SupervisorError,
)
from aiohasupervisor import SupervisorError
from aiohasupervisor.models import AddonsOptions, Discovery, PartialBackupOptions
import pytest
@@ -24,8 +20,10 @@ from homeassistant.components.hassio.addon_manager import (
from homeassistant.core import HomeAssistant
@pytest.mark.usefixtures("addon_not_installed")
async def test_not_installed_raises_exception(addon_manager: AddonManager) -> None:
async def test_not_installed_raises_exception(
addon_manager: AddonManager,
addon_not_installed: dict[str, Any],
) -> None:
"""Test addon not installed raises exception."""
addon_config = {"test_key": "test"}
@@ -40,40 +38,24 @@ async def test_not_installed_raises_exception(addon_manager: AddonManager) -> No
assert str(err.value) == "Test app is not installed"
@pytest.mark.parametrize(
"exception",
[
AddonNotSupportedArchitectureError(
"Add-on test not supported on this platform, supported architectures: test"
),
AddonNotSupportedHomeAssistantVersionError(
"Add-on test not supported on this system, requires Home Assistant version 2026.1.0 or greater"
),
AddonNotSupportedMachineTypeError(
"Add-on test not supported on this machine, supported machine types: test"
),
],
)
async def test_not_available_raises_exception(
addon_manager: AddonManager,
supervisor_client: AsyncMock,
addon_store_info: AsyncMock,
addon_info: AsyncMock,
exception: SupervisorError,
) -> None:
"""Test addon not available raises exception."""
supervisor_client.store.addon_availability.side_effect = exception
supervisor_client.store.install_addon.side_effect = exception
addon_info.return_value.update_available = True
addon_store_info.return_value.available = False
addon_info.return_value.available = False
with pytest.raises(AddonError) as err:
await addon_manager.async_install_addon()
assert str(err.value) == f"Test app is not available: {exception!s}"
assert str(err.value) == "Test app is not available"
with pytest.raises(AddonError) as err:
await addon_manager.async_update_addon()
assert str(err.value) == f"Test app is not available: {exception!s}"
assert str(err.value) == "Test app is not available"
async def test_get_addon_discovery_info(
@@ -514,10 +496,11 @@ async def test_stop_addon_error(
assert stop_addon.call_count == 1
@pytest.mark.usefixtures("hass", "addon_installed")
async def test_update_addon(
hass: HomeAssistant,
addon_manager: AddonManager,
addon_info: AsyncMock,
addon_installed: AsyncMock,
create_backup: AsyncMock,
update_addon: AsyncMock,
) -> None:
@@ -526,7 +509,7 @@ async def test_update_addon(
await addon_manager.async_update_addon()
assert addon_info.call_count == 1
assert addon_info.call_count == 2
assert create_backup.call_count == 1
assert create_backup.call_args == call(
PartialBackupOptions(name="addon_test_addon_1.0.0", addons={"test_addon"})
@@ -534,10 +517,10 @@ async def test_update_addon(
assert update_addon.call_count == 1
@pytest.mark.usefixtures("addon_installed")
async def test_update_addon_no_update(
addon_manager: AddonManager,
addon_info: AsyncMock,
addon_installed: AsyncMock,
create_backup: AsyncMock,
update_addon: AsyncMock,
) -> None:
@@ -551,10 +534,11 @@ async def test_update_addon_no_update(
assert update_addon.call_count == 0
@pytest.mark.usefixtures("hass", "addon_installed")
async def test_update_addon_error(
hass: HomeAssistant,
addon_manager: AddonManager,
addon_info: AsyncMock,
addon_installed: AsyncMock,
create_backup: AsyncMock,
update_addon: AsyncMock,
) -> None:
@@ -567,7 +551,7 @@ async def test_update_addon_error(
assert str(err.value) == "Failed to update the Test app: Boom"
assert addon_info.call_count == 1
assert addon_info.call_count == 2
assert create_backup.call_count == 1
assert create_backup.call_args == call(
PartialBackupOptions(name="addon_test_addon_1.0.0", addons={"test_addon"})
@@ -575,10 +559,11 @@ async def test_update_addon_error(
assert update_addon.call_count == 1
@pytest.mark.usefixtures("hass", "addon_installed")
async def test_schedule_update_addon(
hass: HomeAssistant,
addon_manager: AddonManager,
addon_info: AsyncMock,
addon_installed: AsyncMock,
create_backup: AsyncMock,
update_addon: AsyncMock,
) -> None:
@@ -604,7 +589,7 @@ async def test_schedule_update_addon(
await asyncio.gather(update_task, update_task_two)
assert addon_manager.task_in_progress() is False
assert addon_info.call_count == 2
assert addon_info.call_count == 3
assert create_backup.call_count == 1
assert create_backup.call_args == call(
PartialBackupOptions(name="addon_test_addon_1.0.0", addons={"test_addon"})

View File

@@ -906,6 +906,7 @@ async def test_config_flow_thread_addon_already_installed(
}
@pytest.mark.usefixtures("addon_not_installed")
async def test_options_flow_zigbee_to_thread(
hass: HomeAssistant,
install_addon: AsyncMock,

View File

@@ -445,15 +445,17 @@ async def test_zeroconf_not_onboarded_installed(
]
],
)
@pytest.mark.usefixtures("supervisor", "addon_not_installed", "not_onboarded")
async def test_zeroconf_not_onboarded_not_installed(
hass: HomeAssistant,
supervisor: MagicMock,
addon_info: AsyncMock,
addon_store_info: AsyncMock,
addon_not_installed: AsyncMock,
install_addon: AsyncMock,
start_addon: AsyncMock,
client_connect: AsyncMock,
setup_entry: AsyncMock,
not_onboarded: MagicMock,
zeroconf_info: ZeroconfServiceInfo,
) -> None:
"""Test flow Zeroconf discovery when not onboarded and add-on not installed."""
@@ -465,7 +467,7 @@ async def test_zeroconf_not_onboarded_not_installed(
await hass.async_block_till_done()
assert addon_info.call_count == 0
assert addon_store_info.call_count == 1
assert addon_store_info.call_count == 2
assert install_addon.call_args == call("core_matter_server")
assert start_addon.call_args == call("core_matter_server")
assert client_connect.call_count == 1

View File

@@ -258,7 +258,10 @@ async def test_listen_failure_config_entry_loaded(
async def test_raise_addon_task_in_progress(
hass: HomeAssistant, install_addon: AsyncMock, start_addon: AsyncMock
hass: HomeAssistant,
addon_not_installed: AsyncMock,
install_addon: AsyncMock,
start_addon: AsyncMock,
) -> None:
"""Test raise ConfigEntryNotReady if an add-on task is in progress."""
install_event = asyncio.Event()
@@ -334,6 +337,7 @@ async def test_start_addon(
async def test_install_addon(
hass: HomeAssistant,
addon_not_installed: AsyncMock,
addon_store_info: AsyncMock,
install_addon: AsyncMock,
start_addon: AsyncMock,
@@ -353,7 +357,7 @@ async def test_install_addon(
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_RETRY
assert addon_store_info.call_count == 2
assert addon_store_info.call_count == 3
assert install_addon.call_count == 1
assert install_addon.call_args == call("core_matter_server")
assert start_addon.call_count == 1

View File

@@ -2,7 +2,7 @@
from unittest.mock import Mock, patch
from aiohttp import ClientResponseError
from aiohttp import ClientError, ClientResponseError
import pytest
from yalexs.exceptions import InvalidAuth, YaleApiError
@@ -17,7 +17,11 @@ from homeassistant.const import (
STATE_ON,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.exceptions import (
HomeAssistantError,
OAuth2TokenRequestReauthError,
OAuth2TokenRequestTransientError,
)
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
@@ -254,3 +258,58 @@ async def test_oauth_implementation_not_available(hass: HomeAssistant) -> None:
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_RETRY
async def test_oauth_token_request_reauth_error(hass: HomeAssistant) -> None:
"""Test OAuth token request reauth error starts a reauth flow."""
entry = await mock_yale_config_entry(hass)
with patch(
"homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session.async_ensure_token_valid",
side_effect=OAuth2TokenRequestReauthError(
request_info=Mock(real_url="https://auth.yale.com/access_token"),
status=401,
domain=DOMAIN,
),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_ERROR
flows = hass.config_entries.flow.async_progress()
assert len(flows) == 1
assert flows[0]["context"]["source"] == "reauth"
async def test_oauth_token_request_transient_error_is_retryable(
hass: HomeAssistant,
) -> None:
"""Test OAuth token transient request error marks entry for setup retry."""
entry = await mock_yale_config_entry(hass)
with patch(
"homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session.async_ensure_token_valid",
side_effect=OAuth2TokenRequestTransientError(
request_info=Mock(real_url="https://auth.yale.com/access_token"),
status=500,
domain=DOMAIN,
),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_RETRY
async def test_oauth_client_error_is_retryable(hass: HomeAssistant) -> None:
"""Test OAuth transport client errors mark entry for setup retry."""
entry = await mock_yale_config_entry(hass)
with patch(
"homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session.async_ensure_token_valid",
side_effect=ClientError("connection error"),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.SETUP_RETRY

View File

@@ -651,7 +651,7 @@ async def test_abort_hassio_discovery_for_other_addon(hass: HomeAssistant) -> No
assert result2["reason"] == "not_zwave_js_addon"
@pytest.mark.usefixtures("supervisor", "addon_info")
@pytest.mark.usefixtures("supervisor", "addon_not_installed", "addon_info")
@pytest.mark.parametrize(
("usb_discovery_info", "device", "discovery_name"),
[
@@ -1176,7 +1176,7 @@ async def test_usb_discovery_migration_restore_driver_ready_timeout(
@pytest.mark.parametrize(
"service_info", [ESPHOME_DISCOVERY_INFO, ESPHOME_DISCOVERY_INFO_CLEAN]
)
@pytest.mark.usefixtures("supervisor", "addon_info")
@pytest.mark.usefixtures("supervisor", "addon_not_installed", "addon_info")
async def test_esphome_discovery_intent_custom(
hass: HomeAssistant,
install_addon: AsyncMock,
@@ -1460,7 +1460,7 @@ async def test_esphome_discovery_already_configured_unmanaged_addon(
}
@pytest.mark.usefixtures("supervisor", "addon_info")
@pytest.mark.usefixtures("supervisor", "addon_not_installed", "addon_info")
async def test_esphome_discovery_usb_same_home_id(
hass: HomeAssistant,
install_addon: AsyncMock,
@@ -1699,7 +1699,7 @@ async def test_discovery_addon_not_running(
assert len(mock_setup_entry.mock_calls) == 1
@pytest.mark.usefixtures("supervisor", "addon_info")
@pytest.mark.usefixtures("supervisor", "addon_not_installed", "addon_info")
async def test_discovery_addon_not_installed(
hass: HomeAssistant,
install_addon: AsyncMock,
@@ -2768,7 +2768,7 @@ async def test_addon_installed_already_configured(
assert entry.data["lr_s2_authenticated_key"] == "new321"
@pytest.mark.usefixtures("supervisor", "addon_info")
@pytest.mark.usefixtures("supervisor", "addon_not_installed", "addon_info")
async def test_addon_not_installed(
hass: HomeAssistant,
install_addon: AsyncMock,
@@ -3873,7 +3873,7 @@ async def test_reconfigure_addon_running_server_info_failure(
assert client.disconnect.call_count == 1
@pytest.mark.usefixtures("supervisor")
@pytest.mark.usefixtures("supervisor", "addon_not_installed")
@pytest.mark.parametrize(
(
"entry_data",
@@ -5036,7 +5036,7 @@ async def test_get_usb_ports_ignored_devices() -> None:
]
@pytest.mark.usefixtures("supervisor", "addon_info")
@pytest.mark.usefixtures("supervisor", "addon_not_installed", "addon_info")
async def test_intent_recommended_user(
hass: HomeAssistant,
install_addon: AsyncMock,
@@ -5132,7 +5132,7 @@ async def test_intent_recommended_user(
assert len(mock_setup_entry.mock_calls) == 1
@pytest.mark.usefixtures("supervisor", "addon_info")
@pytest.mark.usefixtures("supervisor", "addon_not_installed", "addon_info")
@pytest.mark.parametrize(
("usb_discovery_info", "device", "discovery_name"),
[

View File

@@ -933,7 +933,7 @@ async def test_start_addon(
assert start_addon.call_args == call("core_zwave_js")
@pytest.mark.usefixtures("addon_info")
@pytest.mark.usefixtures("addon_not_installed", "addon_info")
async def test_install_addon(
hass: HomeAssistant,
install_addon: AsyncMock,