diff --git a/homeassistant/components/avosdim/__init__.py b/homeassistant/components/avosdim/__init__.py new file mode 100644 index 00000000000000..f48cff6f8f13b0 --- /dev/null +++ b/homeassistant/components/avosdim/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: Avosdim.""" diff --git a/homeassistant/components/avosdim/manifest.json b/homeassistant/components/avosdim/manifest.json new file mode 100644 index 00000000000000..d8f691cfbb16e9 --- /dev/null +++ b/homeassistant/components/avosdim/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "avosdim", + "name": "Avosdim", + "integration_type": "virtual", + "supported_by": "motion_blinds" +} diff --git a/homeassistant/components/blebox/button.py b/homeassistant/components/blebox/button.py index 944d0fdf23e54e..4500eeaeb48ce4 100644 --- a/homeassistant/components/blebox/button.py +++ b/homeassistant/components/blebox/button.py @@ -2,7 +2,7 @@ import blebox_uniapi.button -from homeassistant.components.button import ButtonEntity +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback @@ -13,6 +13,16 @@ PARALLEL_UPDATES = 1 +BUTTON_TYPES: dict[str, ButtonEntityDescription] = { + "up": ButtonEntityDescription(key="up", translation_key="up"), + "down": ButtonEntityDescription(key="down", translation_key="down"), + "fav": ButtonEntityDescription(key="fav", translation_key="fav"), + "open": ButtonEntityDescription(key="open", translation_key="open"), + "close": ButtonEntityDescription(key="close", translation_key="close"), +} + +_DEFAULT_BUTTON = ButtonEntityDescription(key="button") + async def async_setup_entry( hass: HomeAssistant, @@ -35,22 +45,16 @@ def __init__( self, coordinator: BleBoxCoordinator, feature: blebox_uniapi.button.Button ) -> None: """Initialize a BleBox button feature.""" + super().__init__(coordinator, feature) - self._attr_icon = self.get_icon() - - def get_icon(self) -> str | None: - """Return icon for endpoint.""" - if "up" in self._feature.query_string: - return "mdi:arrow-up-circle" - if "down" in self._feature.query_string: - return "mdi:arrow-down-circle" - if "fav" in self._feature.query_string: - return "mdi:heart-circle" - if "open" in self._feature.query_string: - return "mdi:arrow-up-circle" - if "close" in self._feature.query_string: - return "mdi:arrow-down-circle" - return None + self.entity_description = self._get_description() + + def _get_description(self) -> ButtonEntityDescription: + """Return the description matching this button's query string.""" + for key, description in BUTTON_TYPES.items(): + if key in self._feature.query_string: + return description + return _DEFAULT_BUTTON @blebox_command async def async_press(self) -> None: diff --git a/homeassistant/components/blebox/coordinator.py b/homeassistant/components/blebox/coordinator.py index aa151cb5cbe54d..45056bfd29986b 100644 --- a/homeassistant/components/blebox/coordinator.py +++ b/homeassistant/components/blebox/coordinator.py @@ -43,6 +43,6 @@ async def _async_update_data(self) -> None: except Error as err: raise UpdateFailed( translation_domain=DOMAIN, - translation_key="update_failed", + translation_key="data_update_failed", translation_placeholders={"error": str(err)}, ) from err diff --git a/homeassistant/components/blebox/icons.json b/homeassistant/components/blebox/icons.json new file mode 100644 index 00000000000000..1cea7723d6d90f --- /dev/null +++ b/homeassistant/components/blebox/icons.json @@ -0,0 +1,26 @@ +{ + "entity": { + "button": { + "close": { + "default": "mdi:arrow-down-circle" + }, + "down": { + "default": "mdi:arrow-down-circle" + }, + "fav": { + "default": "mdi:heart-circle" + }, + "open": { + "default": "mdi:arrow-up-circle" + }, + "up": { + "default": "mdi:arrow-up-circle" + } + }, + "sensor": { + "power_consumption": { + "default": "mdi:lightning-bolt" + } + } + } +} diff --git a/homeassistant/components/blebox/light.py b/homeassistant/components/blebox/light.py index 6bdb072f8793a5..312097226a3b76 100644 --- a/homeassistant/components/blebox/light.py +++ b/homeassistant/components/blebox/light.py @@ -19,10 +19,11 @@ LightEntityFeature, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from . import BleBoxConfigEntry -from .const import LIGHT_MAX_KELVINS, LIGHT_MIN_KELVINS +from .const import DOMAIN, LIGHT_MAX_KELVINS, LIGHT_MIN_KELVINS from .coordinator import BleBoxCoordinator from .entity import BleBoxEntity from .util import blebox_command @@ -215,8 +216,10 @@ async def async_turn_on(self, **kwargs: Any) -> None: try: await self._feature.async_on(value) except ValueError as exc: - raise ValueError( - f"Turning on '{self.name}' failed: Bad value {value}" + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="bad_value", + translation_placeholders={"error": str(exc)}, ) from exc if effect is not None: @@ -224,9 +227,10 @@ async def async_turn_on(self, **kwargs: Any) -> None: effect_value = self.effect_list.index(effect) await self._feature.async_api_command("effect", effect_value) except ValueError as exc: - raise ValueError( - f"Turning on with effect '{self.name}' failed: {effect} not in" - " effect list." + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="effect_not_found", + translation_placeholders={"error": str(exc)}, ) from exc @blebox_command diff --git a/homeassistant/components/blebox/sensor.py b/homeassistant/components/blebox/sensor.py index 9ea7af8a4d8627..a528f006b6c5a8 100644 --- a/homeassistant/components/blebox/sensor.py +++ b/homeassistant/components/blebox/sensor.py @@ -57,9 +57,9 @@ ), SensorEntityDescription( key="powerConsumption", + translation_key="power_consumption", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, suggested_display_precision=2, - icon="mdi:lightning-bolt", ), SensorEntityDescription( key="humidity", diff --git a/homeassistant/components/blebox/strings.json b/homeassistant/components/blebox/strings.json index a459d77f2e298c..cd13ec0ed1a7b1 100644 --- a/homeassistant/components/blebox/strings.json +++ b/homeassistant/components/blebox/strings.json @@ -36,8 +36,23 @@ } }, "exceptions": { - "update_failed": { + "bad_value": { + "message": "Turning on the light failed: {error}" + }, + "command_failed": { + "message": "Failed to execute command on the BleBox device: {error}" + }, + "data_update_failed": { "message": "An error occurred while communicating with the BleBox device: {error}" + }, + "effect_not_found": { + "message": "The specified light effect is not available on this device: {error}" + }, + "install_failed": { + "message": "Failed to install firmware update on the BleBox device: {error}" + }, + "update_failed": { + "message": "Failed to fetch firmware update information from the BleBox device: {error}" } } } diff --git a/homeassistant/components/blebox/update.py b/homeassistant/components/blebox/update.py index ed974b17576743..f2b95e85c2c71d 100644 --- a/homeassistant/components/blebox/update.py +++ b/homeassistant/components/blebox/update.py @@ -18,6 +18,7 @@ from homeassistant.helpers.event import async_call_later from . import BleBoxConfigEntry +from .const import DOMAIN from .coordinator import BleBoxCoordinator from .entity import BleBoxEntity @@ -86,7 +87,11 @@ async def async_update(self) -> None: try: await self._feature.async_update() except Error as ex: - raise HomeAssistantError(ex) from ex + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="update_failed", + translation_placeholders={"error": str(ex)}, + ) from ex self._sync_sw_version() @property @@ -121,7 +126,11 @@ async def async_install( await self._feature.async_install() except Error as ex: self._reset_progress() - raise HomeAssistantError(ex) from ex + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="install_failed", + translation_placeholders={"error": str(ex)}, + ) from ex self._poll_cancel = async_call_later( self.hass, _POLL_INTERVAL_SECONDS, self._poll_until_updated ) diff --git a/homeassistant/components/blebox/util.py b/homeassistant/components/blebox/util.py index bc80611fdc4c09..b195f1245e76a7 100644 --- a/homeassistant/components/blebox/util.py +++ b/homeassistant/components/blebox/util.py @@ -7,6 +7,7 @@ from homeassistant.exceptions import HomeAssistantError +from .const import DOMAIN from .entity import BleBoxEntity @@ -22,7 +23,11 @@ async def handler(self: _BleBoxEntityT, *args: _P.args, **kwargs: _P.kwargs) -> try: return await func(self, *args, **kwargs) except Error as err: - raise HomeAssistantError(str(err)) from err + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="command_failed", + translation_placeholders={"error": str(err)}, + ) from err finally: await self.coordinator.async_refresh() diff --git a/homeassistant/components/generic/manifest.json b/homeassistant/components/generic/manifest.json index d5ed450df1ded9..33463d54dbd0bb 100644 --- a/homeassistant/components/generic/manifest.json +++ b/homeassistant/components/generic/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/generic", "integration_type": "device", "iot_class": "local_push", - "requirements": ["av==16.0.1", "Pillow==12.2.0"] + "requirements": ["av==17.0.1", "Pillow==12.2.0"] } diff --git a/homeassistant/components/helty/__init__.py b/homeassistant/components/helty/__init__.py index 5e70696bd00c1d..ce6edf055ccc02 100644 --- a/homeassistant/components/helty/__init__.py +++ b/homeassistant/components/helty/__init__.py @@ -7,7 +7,7 @@ from .coordinator import HeltyConfigEntry, HeltyDataUpdateCoordinator -PLATFORMS: list[Platform] = [Platform.FAN, Platform.SENSOR] +PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.FAN, Platform.SENSOR] async def async_setup_entry(hass: HomeAssistant, entry: HeltyConfigEntry) -> bool: diff --git a/homeassistant/components/helty/button.py b/homeassistant/components/helty/button.py new file mode 100644 index 00000000000000..abe5b2a9338cb6 --- /dev/null +++ b/homeassistant/components/helty/button.py @@ -0,0 +1,47 @@ +"""Button platform for the Helty Flow integration.""" + +from pyhelty import HeltyError + +from homeassistant.components.button import ButtonEntity +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .const import DOMAIN +from .coordinator import HeltyConfigEntry, HeltyDataUpdateCoordinator +from .entity import HeltyEntity + +PARALLEL_UPDATES = 0 + + +async def async_setup_entry( + hass: HomeAssistant, + entry: HeltyConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the Helty buttons.""" + async_add_entities([HeltyResetFilterButton(entry.runtime_data)]) + + +class HeltyResetFilterButton(HeltyEntity, ButtonEntity): + """Resets the filter-life counter after the filter has been replaced.""" + + _attr_entity_category = EntityCategory.CONFIG + _attr_translation_key = "reset_filter" + + def __init__(self, coordinator: HeltyDataUpdateCoordinator) -> None: + """Initialize the button.""" + super().__init__(coordinator) + self._attr_unique_id = f"{self._device_id}_reset_filter" + + async def async_press(self) -> None: + """Reset the filter-life counter.""" + try: + await self.coordinator.client.async_reset_filter() + except HeltyError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="reset_filter_failed", + ) from err + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/helty/fan.py b/homeassistant/components/helty/fan.py index 1c22bddf9954cd..a3aab27212c5ae 100644 --- a/homeassistant/components/helty/fan.py +++ b/homeassistant/components/helty/fan.py @@ -2,17 +2,18 @@ from typing import Any -from pyhelty import FanMode +from pyhelty import FanMode, HeltyError from homeassistant.components.fan import FanEntity, FanEntityFeature from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.util.percentage import ( ordered_list_item_to_percentage, percentage_to_ordered_list_item, ) -from .const import PRESET_BOOST, PRESET_FREE_COOLING, PRESET_NIGHT +from .const import DOMAIN, PRESET_BOOST, PRESET_FREE_COOLING, PRESET_NIGHT from .coordinator import HeltyConfigEntry, HeltyDataUpdateCoordinator from .entity import HeltyEntity @@ -116,5 +117,11 @@ async def async_turn_off(self, **kwargs: Any) -> None: await self._async_set_mode(FanMode.OFF) async def _async_set_mode(self, mode: FanMode) -> None: - await self.coordinator.client.async_set_fan_mode(mode) + try: + await self.coordinator.client.async_set_fan_mode(mode) + except HeltyError as err: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="set_fan_mode_failed", + ) from err await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/helty/quality_scale.yaml b/homeassistant/components/helty/quality_scale.yaml index 62540e22776404..b650a2fd1e5871 100644 --- a/homeassistant/components/helty/quality_scale.yaml +++ b/homeassistant/components/helty/quality_scale.yaml @@ -26,9 +26,7 @@ rules: unique-config-entry: done # Silver - action-exceptions: - status: exempt - comment: This integration does not provide additional actions. + action-exceptions: done config-entry-unloading: done docs-configuration-parameters: status: exempt diff --git a/homeassistant/components/helty/strings.json b/homeassistant/components/helty/strings.json index 8d8c1cd17b1d74..0c1d13f5f42223 100644 --- a/homeassistant/components/helty/strings.json +++ b/homeassistant/components/helty/strings.json @@ -20,6 +20,11 @@ } }, "entity": { + "button": { + "reset_filter": { + "name": "Reset filter" + } + }, "sensor": { "indoor_humidity": { "name": "Indoor humidity" @@ -31,5 +36,13 @@ "name": "Outdoor temperature" } } + }, + "exceptions": { + "reset_filter_failed": { + "message": "Failed to reset the filter." + }, + "set_fan_mode_failed": { + "message": "Failed to set the ventilation mode." + } } } diff --git a/homeassistant/components/homee/cover.py b/homeassistant/components/homee/cover.py index 4b56c3a7fd9bbd..1a7008f432d706 100644 --- a/homeassistant/components/homee/cover.py +++ b/homeassistant/components/homee/cover.py @@ -51,6 +51,7 @@ class HomeeCoverState(float, Enum): class HomeeSlatState(float, Enum): """Slat states for covers in homee.""" + STOPPED = 0.0 CLOSED = 1.0 OPEN = 2.0 @@ -82,7 +83,11 @@ def get_cover_features( features |= CoverEntityFeature.SET_POSITION if node.get_attribute_by_type(AttributeType.SLAT_ROTATION_IMPULSE) is not None: - features |= CoverEntityFeature.OPEN_TILT | CoverEntityFeature.CLOSE_TILT + features |= ( + CoverEntityFeature.OPEN_TILT + | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.STOP_TILT + ) if node.get_attribute_by_type(AttributeType.SHUTTER_SLAT_POSITION) is not None: features |= CoverEntityFeature.SET_TILT_POSITION @@ -313,6 +318,15 @@ async def async_close_cover_tilt(self, **kwargs: Any) -> None: else: await self.async_set_homee_value(slat_attribute, HomeeSlatState.OPEN) + async def async_stop_cover_tilt(self, **kwargs: Any) -> None: + """Stop the cover tilt.""" + if ( + slat_attribute := self._node.get_attribute_by_type( + AttributeType.SLAT_ROTATION_IMPULSE + ) + ) is not None: + await self.async_set_homee_value(slat_attribute, HomeeSlatState.STOPPED) + async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: """Move the cover tilt to a specific position.""" if CoverEntityFeature.SET_TILT_POSITION in self.supported_features: diff --git a/homeassistant/components/imgw_pib/manifest.json b/homeassistant/components/imgw_pib/manifest.json index 5637f9588d70dc..eab1dbd55a966e 100644 --- a/homeassistant/components/imgw_pib/manifest.json +++ b/homeassistant/components/imgw_pib/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "quality_scale": "platinum", - "requirements": ["imgw_pib==2.2.0"] + "requirements": ["imgw_pib==2.2.2"] } diff --git a/homeassistant/components/paj_gps/entity.py b/homeassistant/components/paj_gps/entity.py index c766c4f2db3cf0..a307f875df1167 100644 --- a/homeassistant/components/paj_gps/entity.py +++ b/homeassistant/components/paj_gps/entity.py @@ -19,8 +19,8 @@ def __init__(self, coordinator: PajGpsCoordinator, device_id: int) -> None: model = None device_models = self.device.device_models - if device_models and isinstance(device_models[0], dict): - model = device_models[0].get("model") + if device_models: + model = device_models[0].model self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, f"{coordinator.user_id}_{device_id}")}, name=self.device.name or f"PAJ GPS {device_id}", diff --git a/homeassistant/components/paj_gps/manifest.json b/homeassistant/components/paj_gps/manifest.json index f6f41a30788d0a..872572b8c0136c 100644 --- a/homeassistant/components/paj_gps/manifest.json +++ b/homeassistant/components/paj_gps/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "quality_scale": "bronze", - "requirements": ["pajgps-api==0.3.1"] + "requirements": ["pajgps-api==0.4.0"] } diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 6701822ec7ba90..75ce0b5507355e 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -565,7 +565,7 @@ def _parse_source_state(self, state: State, coordinates: State) -> None: self._latitude = coordinates.attributes.get(ATTR_LATITUDE) self._longitude = coordinates.attributes.get(ATTR_LONGITUDE) self._gps_accuracy = coordinates.attributes.get(ATTR_GPS_ACCURACY) - self._in_zones = coordinates.attributes.get(ATTR_IN_ZONES, []) + self._in_zones = state.attributes.get(ATTR_IN_ZONES, []) @callback def _update_extra_state_attributes(self) -> None: diff --git a/homeassistant/components/sleepiq/__init__.py b/homeassistant/components/sleepiq/__init__.py index b36637a981256d..742980259bfedc 100644 --- a/homeassistant/components/sleepiq/__init__.py +++ b/homeassistant/components/sleepiq/__init__.py @@ -16,7 +16,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, entity_registry as er -from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant.helpers.typing import ConfigType from .const import DOMAIN, IS_IN_BED, SLEEP_NUMBER @@ -69,7 +69,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: SleepIQConfigEntry) -> b email = conf[CONF_USERNAME] password = conf[CONF_PASSWORD] - client_session = async_get_clientsession(hass) + client_session = async_create_clientsession(hass) gateway = AsyncSleepIQ(client_session=client_session) diff --git a/homeassistant/components/sleepiq/coordinator.py b/homeassistant/components/sleepiq/coordinator.py index d15094049cf58e..4e923eef795d37 100644 --- a/homeassistant/components/sleepiq/coordinator.py +++ b/homeassistant/components/sleepiq/coordinator.py @@ -47,11 +47,16 @@ async def _async_update_data(self) -> None: bed.foundation.update_foundation_status() for bed in self.client.beds.values() ] - await asyncio.gather(*tasks) + try: + await asyncio.gather(*tasks) + except SleepIQTimeoutException as err: + raise UpdateFailed(f"Timed out fetching SleepIQ data: {err}") from err + except SleepIQAPIException as err: + raise UpdateFailed(f"Failed to fetch SleepIQ data: {err}") from err class SleepIQPauseUpdateCoordinator(DataUpdateCoordinator[None]): - """SleepIQ data update coordinator.""" + """SleepIQ pause update coordinator.""" config_entry: SleepIQConfigEntry @@ -72,9 +77,14 @@ def __init__( self.client = client async def _async_update_data(self) -> None: - await asyncio.gather( - *[bed.fetch_pause_mode() for bed in self.client.beds.values()] - ) + try: + await asyncio.gather( + *[bed.fetch_pause_mode() for bed in self.client.beds.values()] + ) + except SleepIQTimeoutException as err: + raise UpdateFailed(f"Timed out fetching SleepIQ pause data: {err}") from err + except SleepIQAPIException as err: + raise UpdateFailed(f"Failed to fetch SleepIQ pause data: {err}") from err class SleepIQSleepDataCoordinator(DataUpdateCoordinator[None]): diff --git a/homeassistant/components/stream/manifest.json b/homeassistant/components/stream/manifest.json index b47763a3ab2938..b9cc560699d41e 100644 --- a/homeassistant/components/stream/manifest.json +++ b/homeassistant/components/stream/manifest.json @@ -7,5 +7,5 @@ "integration_type": "system", "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["PyTurboJPEG==1.8.3", "av==16.0.1", "numpy==2.3.2"] + "requirements": ["PyTurboJPEG==1.8.3", "av==17.0.1", "numpy==2.3.2"] } diff --git a/homeassistant/components/swisscom/__init__.py b/homeassistant/components/swisscom/__init__.py index 5e0c11af090c3e..bb277c1e7c2b42 100644 --- a/homeassistant/components/swisscom/__init__.py +++ b/homeassistant/components/swisscom/__init__.py @@ -1 +1,23 @@ -"""The swisscom component.""" +"""The Swisscom Internet-Box integration.""" + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .coordinator import SwisscomConfigEntry, SwisscomDataUpdateCoordinator + +PLATFORMS: list[Platform] = [Platform.DEVICE_TRACKER] + + +async def async_setup_entry(hass: HomeAssistant, entry: SwisscomConfigEntry) -> bool: + """Set up Swisscom Internet-Box from a config entry.""" + coordinator = SwisscomDataUpdateCoordinator(hass, entry) + await coordinator.async_config_entry_first_refresh() + entry.runtime_data = coordinator + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: SwisscomConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/swisscom/config_flow.py b/homeassistant/components/swisscom/config_flow.py new file mode 100644 index 00000000000000..45ee3848e51380 --- /dev/null +++ b/homeassistant/components/swisscom/config_flow.py @@ -0,0 +1,67 @@ +"""Config flow for the Swisscom Internet-Box integration.""" + +import logging +from typing import Any + +from swisscom_internet_box import ( + SwisscomAuthError, + SwisscomClient, + SwisscomConnectionError, +) +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import format_mac + +from .const import DEFAULT_HOST, DEFAULT_USERNAME, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST, default=DEFAULT_HOST): str, + vol.Required(CONF_USERNAME, default=DEFAULT_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } +) + + +class SwisscomConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a config flow for Swisscom Internet-Box.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle the user step.""" + errors: dict[str, str] = {} + if user_input is not None: + client = SwisscomClient( + async_get_clientsession(self.hass), + user_input[CONF_HOST], + user_input[CONF_USERNAME], + user_input[CONF_PASSWORD], + ) + try: + await client.login() + info = await client.get_box_info() + except SwisscomAuthError: + errors["base"] = "invalid_auth" + except SwisscomConnectionError: + errors["base"] = "cannot_connect" + except Exception: + _LOGGER.exception("Unexpected exception during Swisscom config flow") + errors["base"] = "unknown" + else: + await self.async_set_unique_id(format_mac(info.base_mac)) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=info.model_name or "Internet-Box", data=user_input + ) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/swisscom/const.py b/homeassistant/components/swisscom/const.py new file mode 100644 index 00000000000000..9bc400a268e812 --- /dev/null +++ b/homeassistant/components/swisscom/const.py @@ -0,0 +1,6 @@ +"""Constants for the Swisscom Internet-Box integration.""" + +DOMAIN = "swisscom" + +DEFAULT_HOST = "192.168.1.1" +DEFAULT_USERNAME = "admin" diff --git a/homeassistant/components/swisscom/coordinator.py b/homeassistant/components/swisscom/coordinator.py new file mode 100644 index 00000000000000..c59f24dc05e4f3 --- /dev/null +++ b/homeassistant/components/swisscom/coordinator.py @@ -0,0 +1,59 @@ +"""DataUpdateCoordinator for the Swisscom Internet-Box.""" + +from datetime import timedelta +import logging + +from swisscom_internet_box import ( + Device, + SwisscomAuthError, + SwisscomClient, + SwisscomConnectionError, +) + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=30) + +type SwisscomConfigEntry = ConfigEntry[SwisscomDataUpdateCoordinator] + + +class SwisscomDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Device]]): + """Poll the Internet-Box for the list of LAN devices.""" + + config_entry: SwisscomConfigEntry + + def __init__(self, hass: HomeAssistant, entry: SwisscomConfigEntry) -> None: + """Initialize the coordinator.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + config_entry=entry, + ) + self.client = SwisscomClient( + async_get_clientsession(hass), + entry.data[CONF_HOST], + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + ) + + async def _async_update_data(self) -> dict[str, Device]: + """Fetch device data from the box.""" + try: + devices = await self.client.get_devices() + except SwisscomAuthError as err: + raise ConfigEntryAuthFailed(str(err)) from err + except SwisscomConnectionError as err: + raise UpdateFailed(str(err)) from err + + return {device.key: device for device in devices if device.key} diff --git a/homeassistant/components/swisscom/device_tracker.py b/homeassistant/components/swisscom/device_tracker.py index 902449c9f86474..b8acdb1acd8984 100644 --- a/homeassistant/components/swisscom/device_tracker.py +++ b/homeassistant/components/swisscom/device_tracker.py @@ -1,111 +1,115 @@ -"""Support for Swisscom routers (Internet-Box).""" +"""Device tracker for the Swisscom Internet-Box.""" -from contextlib import suppress -import logging - -import requests import voluptuous as vol from homeassistant.components.device_tracker import ( - DOMAIN as DEVICE_TRACKER_DOMAIN, PLATFORM_SCHEMA as DEVICE_TRACKER_PLATFORM_SCHEMA, - DeviceScanner, + AsyncSeeCallback, + ScannerEntity, ) from homeassistant.const import CONF_HOST -from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.typing import ConfigType - -_LOGGER = logging.getLogger(__name__) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv, issue_registry as ir +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.update_coordinator import CoordinatorEntity -DEFAULT_IP = "192.168.1.1" +from .const import DEFAULT_HOST, DOMAIN +from .coordinator import SwisscomConfigEntry, SwisscomDataUpdateCoordinator PLATFORM_SCHEMA = DEVICE_TRACKER_PLATFORM_SCHEMA.extend( - {vol.Optional(CONF_HOST, default=DEFAULT_IP): cv.string} + {vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string} ) -def get_scanner( - hass: HomeAssistant, config: ConfigType -) -> SwisscomDeviceScanner | None: - """Return the Swisscom device scanner.""" - scanner = SwisscomDeviceScanner(config[DEVICE_TRACKER_DOMAIN]) - - return scanner if scanner.success_init else None - - -class SwisscomDeviceScanner(DeviceScanner): - """Class which queries a router running Swisscom Internet-Box firmware.""" - - def __init__(self, config): - """Initialize the scanner.""" - self.host = config[CONF_HOST] - self.last_results = {} - - # Test the router is accessible. - data = self.get_swisscom_data() - self.success_init = data is not None - - def scan_devices(self): - """Scan for new devices and return a list with found device IDs.""" - self._update_info() - return [client["mac"] for client in self.last_results] - - def get_device_name(self, device): - """Return the name of the given device or None if we don't know.""" - if not self.last_results: - return None - for client in self.last_results: - if client["mac"] == device: - return client["host"] - return None - - def _update_info(self): - """Ensure the information from the Swisscom router is up to date. - - Return boolean if scanning successful. - """ - if not self.success_init: - return False - - _LOGGER.debug("Loading data from Swisscom Internet Box") - if not (data := self.get_swisscom_data()): - return False - - active_clients = [client for client in data.values() if client["status"]] - self.last_results = active_clients - return True - - def get_swisscom_data(self): - """Retrieve data from Swisscom and return parsed result.""" - url = f"http://{self.host}/ws" - headers = {"Content-Type": "application/x-sah-ws-4-call+json"} - data = """ - {"service":"Devices", "method":"get", - "parameters":{"expression":"lan and not self"}}""" - - devices = {} - - try: - request = requests.post(url, headers=headers, data=data, timeout=10) - except ( - requests.exceptions.ConnectionError, - requests.exceptions.Timeout, - requests.exceptions.ConnectTimeout, - ): - _LOGGER.debug("No response from Swisscom Internet Box") - return devices - - if "status" not in request.json(): - _LOGGER.debug("No status in response from Swisscom Internet Box") - return devices - - for device in request.json()["status"]: - with suppress(KeyError, requests.exceptions.RequestException): - devices[device["Key"]] = { - "ip": device["IPAddress"], - "mac": device["PhysAddress"], - "host": device["Name"], - "status": device["Active"], - } - return devices +async def async_setup_scanner( + hass: HomeAssistant, + config: ConfigType, + async_see: AsyncSeeCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> bool: + """Inform users that the YAML configuration is no longer supported.""" + ir.async_create_issue( + hass, + DOMAIN, + "deprecated_yaml_import_issue_credentials_required", + breaks_in_ha_version="2027.1.0", + is_fixable=False, + is_persistent=False, + issue_domain=DOMAIN, + severity=ir.IssueSeverity.WARNING, + translation_key="deprecated_yaml_import_issue_credentials_required", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Swisscom Internet-Box", + "host": config[CONF_HOST], + }, + ) + return False + + +async def async_setup_entry( + hass: HomeAssistant, + entry: SwisscomConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up device tracker entities for the Swisscom Internet-Box.""" + coordinator = entry.runtime_data + tracked: set[str] = set() + + @callback + def _add_new_entities() -> None: + new_keys = [key for key in coordinator.data if key not in tracked] + if new_keys: + tracked.update(new_keys) + async_add_entities( + SwisscomScannerEntity(coordinator, key) for key in new_keys + ) + + _add_new_entities() + entry.async_on_unload(coordinator.async_add_listener(_add_new_entities)) + + +class SwisscomScannerEntity( + CoordinatorEntity[SwisscomDataUpdateCoordinator], ScannerEntity +): + """A device tracked by the Swisscom Internet-Box.""" + + def __init__(self, coordinator: SwisscomDataUpdateCoordinator, key: str) -> None: + """Initialize the scanner entity.""" + super().__init__(coordinator) + self._key = key + self._attr_unique_id = key + + @property + def _device(self): + return self.coordinator.data.get(self._key) + + @property + def is_connected(self) -> bool: + """Return whether the device is currently connected to the LAN.""" + device = self._device + return bool(device and device.active) + + @property + def mac_address(self) -> str: + """Return the MAC address of the device.""" + device = self._device + return device.phys_address if device else self._key + + @property + def hostname(self) -> str | None: + """Return the hostname of the device.""" + device = self._device + return device.name if device else None + + @property + def ip_address(self) -> str | None: + """Return the IP address of the device.""" + device = self._device + return device.ip_address if device else None + + @property + def name(self) -> str | None: + """Return the friendly name of the device.""" + return self.hostname diff --git a/homeassistant/components/swisscom/manifest.json b/homeassistant/components/swisscom/manifest.json index cf1ea01ea9c04a..8b259e82d90daf 100644 --- a/homeassistant/components/swisscom/manifest.json +++ b/homeassistant/components/swisscom/manifest.json @@ -2,7 +2,9 @@ "domain": "swisscom", "name": "Swisscom Internet-Box", "codeowners": [], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/swisscom", + "integration_type": "hub", "iot_class": "local_polling", - "quality_scale": "legacy" + "requirements": ["python-swisscom-internet-box==0.1.1"] } diff --git a/homeassistant/components/swisscom/strings.json b/homeassistant/components/swisscom/strings.json new file mode 100644 index 00000000000000..5ea96118dd4da8 --- /dev/null +++ b/homeassistant/components/swisscom/strings.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "password": "[%key:common::config_flow::data::password%]", + "username": "[%key:common::config_flow::data::username%]" + }, + "data_description": { + "host": "The hostname or IP address of your Swisscom Internet-Box.", + "password": "The administrator password printed on the bottom of the box.", + "username": "The administrator username, normally \"admin\"." + } + } + } + }, + "issues": { + "deprecated_yaml_import_issue_credentials_required": { + "description": "Configuring the {integration_title} integration through YAML is deprecated. The integration now requires a username and password to authenticate to your Internet-Box, which cannot be safely carried over from YAML.\n\nSet up the integration through the UI to provide your credentials (your existing host `{host}` will need to be re-entered), then remove the `{domain}` entry from your `configuration.yaml` file and restart Home Assistant.", + "title": "The {integration_title} YAML configuration is being removed" + } + } +} diff --git a/homeassistant/components/switchbot_cloud/__init__.py b/homeassistant/components/switchbot_cloud/__init__.py index 557129c3092a7f..e11e431a173f2f 100644 --- a/homeassistant/components/switchbot_cloud/__init__.py +++ b/homeassistant/components/switchbot_cloud/__init__.py @@ -504,9 +504,15 @@ async def _internal_handle_webhook( _LOGGER.debug("Received data from switchbot webhook: %s", repr(data)) device_mac = data["context"]["deviceMac"] - if device_mac not in coordinators_by_id: - _LOGGER.error( - "Received data for unknown entity from switchbot webhook: %s", data + registered_device_macs = [ + coordinator.data.get("deviceMac") or coordinator.data.get("deviceId") + for coordinator in coordinators_by_id.values() + if coordinator.manageable_by_webhook() and coordinator.data is not None + ] + if device_mac not in registered_device_macs: + _LOGGER.debug( + "Received data for an unregistered webhook entity from SwitchBot Webhook: %s", + data, ) return diff --git a/homeassistant/components/tuya/binary_sensor.py b/homeassistant/components/tuya/binary_sensor.py index 4c9cf2601158ef..52520b844ee476 100644 --- a/homeassistant/components/tuya/binary_sensor.py +++ b/homeassistant/components/tuya/binary_sensor.py @@ -267,14 +267,7 @@ class TuyaBinarySensorEntityDescription(BinarySensorEntityDescription): ), TAMPER_BINARY_SENSOR, ), - DeviceCategory.LDCG: ( - TuyaBinarySensorEntityDescription( - key=DPCode.TEMPER_ALARM, - device_class=BinarySensorDeviceClass.TAMPER, - entity_category=EntityCategory.DIAGNOSTIC, - ), - TAMPER_BINARY_SENSOR, - ), + DeviceCategory.LDCG: (TAMPER_BINARY_SENSOR,), DeviceCategory.MC: ( TuyaBinarySensorEntityDescription( key=DPCode.STATUS, diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 5ca6cd6ddce34b..5bc658490e6586 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -1070,7 +1070,7 @@ async def handle_test_condition( # alongside the result. condition_trace = trace.trace_get() try: - with trace.record_template_errors(): + with trace.suppress_template_error_logging(): check_result = condition.async_check(variables=msg.get("variables")) except HomeAssistantError as err: connection.send_error( @@ -1134,7 +1134,7 @@ def evaluate_condition(now: datetime | None) -> None: condition_trace = trace.trace_get() try: - with trace.record_template_errors(): + with trace.suppress_template_error_logging(): new_event_data = {"result": condition.async_check()} except HomeAssistantError as err: new_event_data = {"error": str(err)} diff --git a/homeassistant/components/yardian/binary_sensor.py b/homeassistant/components/yardian/binary_sensor.py index df25ae39078a6c..d8077cf5314052 100644 --- a/homeassistant/components/yardian/binary_sensor.py +++ b/homeassistant/components/yardian/binary_sensor.py @@ -11,9 +11,9 @@ from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from homeassistant.helpers.update_coordinator import CoordinatorEntity from .coordinator import YardianConfigEntry, YardianUpdateCoordinator +from .entity import YardianEntity, YardianZoneEntity @dataclass(kw_only=True, frozen=True) @@ -79,38 +79,30 @@ async def async_setup_entry( """Set up Yardian binary sensors.""" coordinator = config_entry.runtime_data + # 1. Global/Main device sensors entities: list[BinarySensorEntity] = [ YardianBinarySensor(coordinator, description) for description in SENSOR_DESCRIPTIONS ] - zone_descriptions = [ - YardianBinarySensorEntityDescription( + # 2. Zone/Child device sensors + for zone_id in range(len(coordinator.data.zones)): + description = YardianBinarySensorEntityDescription( key=f"zone_enabled_{zone_id}", - translation_key="zone_enabled", + translation_key="enabled", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=False, value_fn=_zone_value_factory(zone_id), - translation_placeholders={"zone": str(zone_id + 1)}, ) - for zone_id in range(len(coordinator.data.zones)) - ] - - entities.extend( - YardianBinarySensor(coordinator, description) - for description in zone_descriptions - ) + entities.append(YardianZoneBinarySensor(coordinator, description, zone_id)) async_add_entities(entities) -class YardianBinarySensor( - CoordinatorEntity[YardianUpdateCoordinator], BinarySensorEntity -): - """Representation of a Yardian binary sensor based on a description.""" +class YardianBinarySensor(YardianEntity, BinarySensorEntity): + """Representation of a Yardian binary sensor assigned to the main device.""" entity_description: YardianBinarySensorEntityDescription - _attr_has_entity_name = True def __init__( self, @@ -121,7 +113,28 @@ def __init__( super().__init__(coordinator) self.entity_description = description self._attr_unique_id = f"{coordinator.yid}-{description.key}" - self._attr_device_info = coordinator.device_info + + @property + def is_on(self) -> bool | None: + """Return the current state based on the description's value function.""" + return self.entity_description.value_fn(self.coordinator) + + +class YardianZoneBinarySensor(YardianZoneEntity, BinarySensorEntity): + """Representation of a Yardian binary sensor assigned to a zone child device.""" + + entity_description: YardianBinarySensorEntityDescription + + def __init__( + self, + coordinator: YardianUpdateCoordinator, + description: YardianBinarySensorEntityDescription, + zone_id: int, + ) -> None: + """Initialize the Yardian zone binary sensor.""" + super().__init__(coordinator, zone_id) + self.entity_description = description + self._attr_unique_id = f"{coordinator.yid}-{description.key}" @property def is_on(self) -> bool | None: diff --git a/homeassistant/components/yardian/const.py b/homeassistant/components/yardian/const.py index b4e75f2367b751..a4b5d5926fb75b 100644 --- a/homeassistant/components/yardian/const.py +++ b/homeassistant/components/yardian/const.py @@ -2,6 +2,6 @@ DOMAIN = "yardian" MANUFACTURER = "Aeon Matrix" -PRODUCT_NAME = "Yardian Smart Sprinkler" +PRODUCT_NAME = "Yardian Smart Sprinkler Controller" DEFAULT_WATERING_DURATION = 6 diff --git a/homeassistant/components/yardian/entity.py b/homeassistant/components/yardian/entity.py new file mode 100644 index 00000000000000..a590d1cea714ca --- /dev/null +++ b/homeassistant/components/yardian/entity.py @@ -0,0 +1,35 @@ +"""Base entities for Yardian integration.""" + +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN, MANUFACTURER +from .coordinator import YardianUpdateCoordinator + + +class YardianEntity(CoordinatorEntity[YardianUpdateCoordinator]): + """Base class for Yardian entities assigned to the main device.""" + + _attr_has_entity_name = True + + def __init__(self, coordinator: YardianUpdateCoordinator) -> None: + """Initialize the main device entity.""" + super().__init__(coordinator) + self._attr_device_info = coordinator.device_info + + +class YardianZoneEntity(CoordinatorEntity[YardianUpdateCoordinator]): + """Base class for Yardian entities assigned to a zone child device.""" + + _attr_has_entity_name = True + + def __init__(self, coordinator: YardianUpdateCoordinator, zone_id: int) -> None: + """Initialize the zone device entity.""" + super().__init__(coordinator) + self._zone_id = zone_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, f"{coordinator.yid}_{zone_id}")}, + name=coordinator.data.zones[zone_id].name, + manufacturer=MANUFACTURER, + via_device=(DOMAIN, coordinator.yid), + ) diff --git a/homeassistant/components/yardian/manifest.json b/homeassistant/components/yardian/manifest.json index b76f4122c78847..5a4f8525587b81 100644 --- a/homeassistant/components/yardian/manifest.json +++ b/homeassistant/components/yardian/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/yardian", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["pyyardian==1.3.3"] + "requirements": ["pyyardian==1.4.0"] } diff --git a/homeassistant/components/yardian/sensor.py b/homeassistant/components/yardian/sensor.py index 428a2f5e05d898..86bcb9ae3cfadb 100644 --- a/homeassistant/components/yardian/sensor.py +++ b/homeassistant/components/yardian/sensor.py @@ -13,10 +13,10 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import StateType -from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt as dt_util from .coordinator import YardianConfigEntry, YardianUpdateCoordinator +from .entity import YardianEntity # Values above this threshold indicate the API returned an absolute # timestamp instead of a relative delay, so convert to a remaining delta. @@ -99,11 +99,10 @@ async def async_setup_entry( ) -class YardianSensor(CoordinatorEntity[YardianUpdateCoordinator], SensorEntity): +class YardianSensor(YardianEntity, SensorEntity): """Representation of a Yardian sensor defined by description.""" entity_description: YardianSensorEntityDescription - _attr_has_entity_name = True def __init__( self, @@ -114,7 +113,6 @@ def __init__( super().__init__(coordinator) self.entity_description = description self._attr_unique_id = f"{coordinator.yid}_{description.key}" - self._attr_device_info = coordinator.device_info @property def native_value(self) -> StateType: diff --git a/homeassistant/components/yardian/strings.json b/homeassistant/components/yardian/strings.json index 5951b197c92ac4..50db7dd91cf607 100644 --- a/homeassistant/components/yardian/strings.json +++ b/homeassistant/components/yardian/strings.json @@ -32,7 +32,7 @@ "name": "Watering running" }, "zone_enabled": { - "name": "Zone {zone} enabled" + "name": "Enabled" } }, "sensor": { diff --git a/homeassistant/components/yardian/switch.py b/homeassistant/components/yardian/switch.py index 67b53bd2fc3ce4..4a57da991f6cf0 100644 --- a/homeassistant/components/yardian/switch.py +++ b/homeassistant/components/yardian/switch.py @@ -9,10 +9,10 @@ from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import VolDictType -from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DEFAULT_WATERING_DURATION from .coordinator import YardianConfigEntry, YardianUpdateCoordinator +from .entity import YardianZoneEntity SERVICE_START_IRRIGATION = "start_irrigation" SERVICE_SCHEMA_START_IRRIGATION: VolDictType = { @@ -43,23 +43,15 @@ async def async_setup_entry( ) -class YardianSwitch(CoordinatorEntity[YardianUpdateCoordinator], SwitchEntity): +class YardianSwitch(YardianZoneEntity, SwitchEntity): """Representation of a Yardian switch.""" - _attr_has_entity_name = True - _attr_translation_key = "switch" + _attr_name = None - def __init__(self, coordinator: YardianUpdateCoordinator, zone_id) -> None: + def __init__(self, coordinator: YardianUpdateCoordinator, zone_id: int) -> None: """Initialize a Yardian Switch Device.""" - super().__init__(coordinator) - self._zone_id = zone_id + super().__init__(coordinator, zone_id) self._attr_unique_id = f"{coordinator.yid}-{zone_id}" - self._attr_device_info = coordinator.device_info - - @property - def name(self) -> str: - """Return the zone name.""" - return self.coordinator.data.zones[self._zone_id].name @property def is_on(self) -> bool: diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 9f9fd09fbf342a..2495b990e34453 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -727,6 +727,7 @@ "sunweg", "surepetcare", "swiss_public_transport", + "swisscom", "switchbee", "switchbot", "switchbot_cloud", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 882682864a4758..2acd1b0cfbd215 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -665,6 +665,11 @@ "config_flow": false, "iot_class": "assumed_state" }, + "avosdim": { + "name": "Avosdim", + "integration_type": "virtual", + "supported_by": "motion_blinds" + }, "awair": { "name": "Awair", "integration_type": "hub", @@ -6903,7 +6908,7 @@ "swisscom": { "name": "Swisscom Internet-Box", "integration_type": "hub", - "config_flow": false, + "config_flow": true, "iot_class": "local_polling" }, "switchbee": { diff --git a/homeassistant/helpers/template/__init__.py b/homeassistant/helpers/template/__init__.py index 7bd5c16000f425..720506e94bcd4f 100644 --- a/homeassistant/helpers/template/__init__.py +++ b/homeassistant/helpers/template/__init__.py @@ -25,7 +25,7 @@ from homeassistant.exceptions import TemplateError from homeassistant.helpers.singleton import singleton from homeassistant.helpers.trace import ( - record_template_errors_cv, + suppress_template_error_logging_cv, trace_stack_cv, trace_stack_top, ) @@ -632,14 +632,13 @@ def make_logging_undefined( return jinja2.StrictUndefined def _log_with_logger(level: int, msg: str) -> None: - # When a consumer such as the subscribe_condition websocket command has - # opted in, record the error on the active trace element instead of - # logging it, so repeated evaluations don't spam the log. - if record_template_errors_cv.get() and ( - node := trace_stack_top(trace_stack_cv) - ): + # Record the error on the active trace element so it is surfaced in the + # trace. Consumers such as the subscribe_condition websocket command can + # opt in to additionally suppress the (otherwise repeated) log entry. + if node := trace_stack_top(trace_stack_cv): node.add_template_error(msg) - return + if suppress_template_error_logging_cv.get(): + return template, action = template_cv.get() or ("", "rendering or compiling") _LOGGER.log( diff --git a/homeassistant/helpers/trace.py b/homeassistant/helpers/trace.py index 8b21a87fb66c0a..b3ddcd1b37b000 100644 --- a/homeassistant/helpers/trace.py +++ b/homeassistant/helpers/trace.py @@ -139,26 +139,27 @@ def as_dict(self) -> dict[str, Any]: script_execution_cv: ContextVar[StopReason | None] = ContextVar( "script_execution_cv", default=None ) -# When set, template errors are recorded on the active TraceElement instead of -# being logged directly -record_template_errors_cv: ContextVar[bool] = ContextVar( - "record_template_errors_cv", default=False +# When set, template errors recorded on the active TraceElement are not also +# logged. Template errors are always recorded in the trace regardless. +suppress_template_error_logging_cv: ContextVar[bool] = ContextVar( + "suppress_template_error_logging_cv", default=False ) @contextmanager -def record_template_errors() -> Generator[None]: - """Record template errors in the active trace instead of logging them. +def suppress_template_error_logging() -> Generator[None]: + """Suppress logging of template errors that are recorded in the trace. - Used by consumers such as the subscribe_condition websocket command, which - re-evaluate a condition repeatedly and forward template errors to the client - via the trace, so the errors don't spam the log. + Template errors are always recorded on the active trace element. Consumers + such as the subscribe_condition websocket command, which re-evaluate a + condition repeatedly and forward template errors to the client via the + trace, can use this to also stop the errors from spamming the log. """ - token = record_template_errors_cv.set(True) + token = suppress_template_error_logging_cv.set(True) try: yield finally: - record_template_errors_cv.reset(token) + suppress_template_error_logging_cv.reset(token) def trace_id_set(trace_id: tuple[str, str]) -> None: diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 919118abf6f5ca..c38473c183d975 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -17,7 +17,7 @@ async-upnp-client==0.46.2 atomicwrites-homeassistant==1.4.1 attrs==26.1.0 audioop-lts==0.2.2 -av==16.0.1 +av==17.0.1 awesomeversion==25.8.0 bcrypt==5.0.0 bleak-retry-connector==4.6.1 diff --git a/pylint/plugins/pylint_home_assistant/__init__.py b/pylint/plugins/pylint_home_assistant/__init__.py index 0f799b1333aa2d..c6dd7f5febb8eb 100644 --- a/pylint/plugins/pylint_home_assistant/__init__.py +++ b/pylint/plugins/pylint_home_assistant/__init__.py @@ -8,17 +8,38 @@ import importlib import pkgutil +from pylint.checkers import BaseChecker from pylint.lint import PyLinter from pylint_home_assistant import checkers def register(linter: PyLinter) -> None: - """Register all Home Assistant checkers.""" + """Register all Home Assistant checkers which have not been registered yet.""" + existing_checker_types: set[type] = { + type(checker) + for checkers_list in linter._checkers.values() # noqa: SLF001 + for checker in checkers_list + } # Auto-discover and register all checker modules under ``checkers/``. for module_info in pkgutil.walk_packages( checkers.__path__, prefix=f"{checkers.__name__}." ): module = importlib.import_module(module_info.name) - if hasattr(module, "register"): - module.register(linter) + if not hasattr(module, "register"): + continue + # Skip modules whose checker class is already registered (worker + # re-registration in parallel mode). Only consider checker classes + # defined in the module itself, not ones imported from pylint. + module_checker_types = { + value + for value in vars(module).values() + if ( + isinstance(value, type) + and issubclass(value, BaseChecker) + and value.__module__ == module.__name__ + ) + } + if module_checker_types and module_checker_types <= existing_checker_types: + continue + module.register(linter) diff --git a/requirements_all.txt b/requirements_all.txt index d7c309796cf3c1..d3eed24ff8c54e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -600,7 +600,7 @@ autoskope_client==1.4.1 # homeassistant.components.generic # homeassistant.components.stream -av==16.0.1 +av==17.0.1 # homeassistant.components.avea avea==1.8.0 @@ -1347,7 +1347,7 @@ ihcsdk==2.8.12 imeon_inverter_api==0.4.0 # homeassistant.components.imgw_pib -imgw_pib==2.2.0 +imgw_pib==2.2.2 # homeassistant.components.incomfort incomfort-client==0.7.0 @@ -1806,7 +1806,7 @@ p1monitor==3.2.0 paho-mqtt==2.1.0 # homeassistant.components.paj_gps -pajgps-api==0.3.1 +pajgps-api==0.4.0 # homeassistant.components.panasonic_bluray panacotta==0.2 @@ -2738,6 +2738,9 @@ python-snoo==0.8.3 # homeassistant.components.songpal python-songpal==0.16.2 +# homeassistant.components.swisscom +python-swisscom-internet-box==0.1.1 + # homeassistant.components.tado python-tado==0.18.16 @@ -2836,7 +2839,7 @@ pyws66i==1.1 pyxeoma==1.4.2 # homeassistant.components.yardian -pyyardian==1.3.3 +pyyardian==1.4.0 # homeassistant.components.qrcode pyzbar==0.1.9 diff --git a/tests/components/ai_task/test_init.py b/tests/components/ai_task/test_init.py index 5acf8d8b446537..4286cb62991e4a 100644 --- a/tests/components/ai_task/test_init.py +++ b/tests/components/ai_task/test_init.py @@ -10,7 +10,11 @@ from homeassistant.components import media_source from homeassistant.components.ai_task import AITaskPreferences -from homeassistant.components.ai_task.const import DATA_MEDIA_SOURCE, DATA_PREFERENCES +from homeassistant.components.ai_task.const import ( + DATA_MEDIA_SOURCE, + DATA_PREFERENCES, + DOMAIN, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import selector @@ -95,7 +99,7 @@ async def test_generate_data_service( ), ): result = await hass.services.async_call( - "ai_task", + DOMAIN, "generate_data", { "task_name": "Test Name", @@ -130,7 +134,7 @@ async def test_generate_data_service_structure_fields( ) -> None: """Test the entity can generate structured data with a top level object schema.""" result = await hass.services.async_call( - "ai_task", + DOMAIN, "generate_data", { "task_name": "Profile Generation", @@ -280,7 +284,7 @@ async def test_generate_data_service_invalid_structure( """Test the entity can generate structured data.""" with pytest.raises(expected_exception, match=expected_error): await hass.services.async_call( - "ai_task", + DOMAIN, "generate_data", { "task_name": "Profile Generation", @@ -322,7 +326,7 @@ async def test_generate_image_service( return_value="media-source://ai_task/image/2025-06-14_225900_test_task.png", ) as mock_upload_media: result = await hass.services.async_call( - "ai_task", + DOMAIN, "generate_image", { "task_name": "Test Image", @@ -361,7 +365,7 @@ async def test_generate_image_service_no_entity( match="No entity_id provided and no preferred entity set", ): await hass.services.async_call( - "ai_task", + DOMAIN, "generate_image", { "task_name": "Test Image", diff --git a/tests/components/alarm_control_panel/test_reproduce_state.py b/tests/components/alarm_control_panel/test_reproduce_state.py index fcb4fdee36e9ef..8e2fee52bd5342 100644 --- a/tests/components/alarm_control_panel/test_reproduce_state.py +++ b/tests/components/alarm_control_panel/test_reproduce_state.py @@ -2,7 +2,7 @@ import pytest -from homeassistant.components.alarm_control_panel import AlarmControlPanelState +from homeassistant.components.alarm_control_panel import DOMAIN, AlarmControlPanelState from homeassistant.const import ( SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_CUSTOM_BYPASS, @@ -56,25 +56,15 @@ async def test_reproducing_states( {}, ) - arm_away_calls = async_mock_service( - hass, "alarm_control_panel", SERVICE_ALARM_ARM_AWAY - ) + arm_away_calls = async_mock_service(hass, DOMAIN, SERVICE_ALARM_ARM_AWAY) arm_custom_bypass_calls = async_mock_service( - hass, "alarm_control_panel", SERVICE_ALARM_ARM_CUSTOM_BYPASS - ) - arm_home_calls = async_mock_service( - hass, "alarm_control_panel", SERVICE_ALARM_ARM_HOME - ) - arm_night_calls = async_mock_service( - hass, "alarm_control_panel", SERVICE_ALARM_ARM_NIGHT - ) - arm_vacation_calls = async_mock_service( - hass, "alarm_control_panel", SERVICE_ALARM_ARM_VACATION - ) - disarm_calls = async_mock_service(hass, "alarm_control_panel", SERVICE_ALARM_DISARM) - trigger_calls = async_mock_service( - hass, "alarm_control_panel", SERVICE_ALARM_TRIGGER + hass, DOMAIN, SERVICE_ALARM_ARM_CUSTOM_BYPASS ) + arm_home_calls = async_mock_service(hass, DOMAIN, SERVICE_ALARM_ARM_HOME) + arm_night_calls = async_mock_service(hass, DOMAIN, SERVICE_ALARM_ARM_NIGHT) + arm_vacation_calls = async_mock_service(hass, DOMAIN, SERVICE_ALARM_ARM_VACATION) + disarm_calls = async_mock_service(hass, DOMAIN, SERVICE_ALARM_DISARM) + trigger_calls = async_mock_service(hass, DOMAIN, SERVICE_ALARM_TRIGGER) # These calls should do nothing as entities already in desired state await async_reproduce_state( diff --git a/tests/components/alert/test_reproduce_state.py b/tests/components/alert/test_reproduce_state.py index e0f817443dc582..f2368a6a2ad1aa 100644 --- a/tests/components/alert/test_reproduce_state.py +++ b/tests/components/alert/test_reproduce_state.py @@ -2,6 +2,7 @@ import pytest +from homeassistant.components.alert import DOMAIN from homeassistant.core import HomeAssistant, State from homeassistant.helpers.state import async_reproduce_state @@ -15,8 +16,8 @@ async def test_reproducing_states( hass.states.async_set("alert.entity_off", "off", {}) hass.states.async_set("alert.entity_on", "on", {}) - turn_on_calls = async_mock_service(hass, "alert", "turn_on") - turn_off_calls = async_mock_service(hass, "alert", "turn_off") + turn_on_calls = async_mock_service(hass, DOMAIN, "turn_on") + turn_off_calls = async_mock_service(hass, DOMAIN, "turn_off") # These calls should do nothing as entities already in desired state await async_reproduce_state( diff --git a/tests/components/assist_satellite/test_entity.py b/tests/components/assist_satellite/test_entity.py index 60d0541a4b3eea..09e7ed6b781591 100644 --- a/tests/components/assist_satellite/test_entity.py +++ b/tests/components/assist_satellite/test_entity.py @@ -24,7 +24,7 @@ AssistSatelliteAnswer, SatelliteBusyError, ) -from homeassistant.components.assist_satellite.const import PREANNOUNCE_URL +from homeassistant.components.assist_satellite.const import DOMAIN, PREANNOUNCE_URL from homeassistant.components.assist_satellite.entity import AssistSatelliteState from homeassistant.components.media_source import PlayMedia from homeassistant.config_entries import ConfigEntry @@ -362,7 +362,7 @@ def tts_generate_media_source_id( patch.object(entity, "async_announce", new=async_announce), ): await hass.services.async_call( - "assist_satellite", + DOMAIN, "announce", service_data, target={"entity_id": "assist_satellite.test_entity"}, @@ -457,7 +457,7 @@ async def async_announce(announcement): with patch.object(entity, "async_announce", new=async_announce): await hass.services.async_call( - "assist_satellite", + DOMAIN, "announce", {"media_id": "test-media-id"}, target={"entity_id": "assist_satellite.test_entity"}, @@ -777,7 +777,7 @@ async def async_start_conversation(start_announcement): patch.object(entity, "async_start_conversation", new=async_start_conversation), ): await hass.services.async_call( - "assist_satellite", + DOMAIN, "start_conversation", service_data, target={"entity_id": "assist_satellite.test_entity"}, @@ -796,7 +796,7 @@ async def test_start_conversation_reject_builtin_agent( """Test starting a conversation on a device.""" with pytest.raises(HomeAssistantError): await hass.services.async_call( - "assist_satellite", + DOMAIN, "start_conversation", {"start_message": "Hey!"}, target={"entity_id": "assist_satellite.test_entity"}, @@ -822,7 +822,7 @@ async def async_start_conversation(start_announcement): patch.object(entity, "async_start_conversation", new=async_start_conversation), ): await hass.services.async_call( - "assist_satellite", + DOMAIN, "start_conversation", {"start_media_id": "test-media-id"}, target={"entity_id": "assist_satellite.test_entity"}, @@ -958,7 +958,7 @@ async def async_start_conversation(start_announcement): patch.object(entity, "async_start_conversation", new=async_start_conversation), ): response = await hass.services.async_call( - "assist_satellite", + DOMAIN, "ask_question", {"entity_id": entity_id, "question": question_text, **service_data}, blocking=True, @@ -977,7 +977,7 @@ async def test_ask_question_requires_entity_permission( """Test ask_question is denied for users without POLICY_CONTROL on the entity.""" with pytest.raises(Unauthorized): await hass.services.async_call( - "assist_satellite", + DOMAIN, "ask_question", {"entity_id": "assist_satellite.test_entity", "question": "Anything?"}, blocking=True, diff --git a/tests/components/automation/test_blueprint.py b/tests/components/automation/test_blueprint.py index 1e7c616efeb061..1e3de5a1acf743 100644 --- a/tests/components/automation/test_blueprint.py +++ b/tests/components/automation/test_blueprint.py @@ -12,6 +12,7 @@ import pytest from homeassistant.components import automation +from homeassistant.components.automation import DOMAIN from homeassistant.components.blueprint import models from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant, callback @@ -141,7 +142,7 @@ def set_person_state(state: str, extra: dict[str, Any]) -> None: # Verify trigger works await hass.services.async_call( - "automation", + DOMAIN, "trigger", {"entity_id": "automation.automation_0"}, blocking=True, @@ -228,7 +229,7 @@ async def test_motion_light(hass: HomeAssistant) -> None: # Verify trigger works await hass.services.async_call( - "automation", + DOMAIN, "trigger", {"entity_id": "automation.automation_0"}, ) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index f19933e1c5efcc..10fe7b26ad4a98 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -262,12 +262,12 @@ async def test_trigger_service_ignoring_condition( assert caplog.record_tuples[0][1] == logging.WARNING await hass.services.async_call( - "automation", "trigger", {"entity_id": "automation.test"}, blocking=True + DOMAIN, "trigger", {"entity_id": "automation.test"}, blocking=True ) assert len(calls) == 1 await hass.services.async_call( - "automation", + DOMAIN, "trigger", {"entity_id": "automation.test", "skip_condition": True}, blocking=True, @@ -275,7 +275,7 @@ async def test_trigger_service_ignoring_condition( assert len(calls) == 2 await hass.services.async_call( - "automation", + DOMAIN, "trigger", {"entity_id": "automation.test", "skip_condition": False}, blocking=True, @@ -1777,7 +1777,7 @@ async def test_automation_bad_config_validation( assert issues[0]["translation_placeholders"]["error"].startswith(details) # Make sure both automations are setup - assert set(hass.states.async_entity_ids("automation")) == { + assert set(hass.states.async_entity_ids(DOMAIN)) == { "automation.bad_automation", "automation.good_automation", } @@ -3187,7 +3187,7 @@ async def test_trigger_service(hass: HomeAssistant, calls: list[ServiceCall]) -> ) context = Context() await hass.services.async_call( - "automation", + DOMAIN, "trigger", {"entity_id": "automation.hello"}, blocking=True, @@ -3684,7 +3684,7 @@ async def test_automation_turns_off_other_automation(hass: HomeAssistant) -> Non assert len(calls) == 0 await hass.services.async_call( - "automation", + DOMAIN, "turn_on", {"entity_id": "automation.automation_1"}, blocking=True, diff --git a/tests/components/automation/test_reproduce_state.py b/tests/components/automation/test_reproduce_state.py index 2b987572ae4520..591a4d0080b18d 100644 --- a/tests/components/automation/test_reproduce_state.py +++ b/tests/components/automation/test_reproduce_state.py @@ -2,6 +2,7 @@ import pytest +from homeassistant.components.automation import DOMAIN from homeassistant.core import HomeAssistant, State from homeassistant.helpers.state import async_reproduce_state @@ -15,8 +16,8 @@ async def test_reproducing_states( hass.states.async_set("automation.entity_off", "off", {}) hass.states.async_set("automation.entity_on", "on", {}) - turn_on_calls = async_mock_service(hass, "automation", "turn_on") - turn_off_calls = async_mock_service(hass, "automation", "turn_off") + turn_on_calls = async_mock_service(hass, DOMAIN, "turn_on") + turn_off_calls = async_mock_service(hass, DOMAIN, "turn_off") # These calls should do nothing as entities already in desired state await async_reproduce_state( diff --git a/tests/components/blebox/test_button.py b/tests/components/blebox/test_button.py index d3aae3d61c8453..a2a6ad517e5042 100644 --- a/tests/components/blebox/test_button.py +++ b/tests/components/blebox/test_button.py @@ -6,17 +6,18 @@ import blebox_uniapi import pytest -from homeassistant.const import ATTR_ICON from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from .conftest import async_setup_entity, mock_feature -query_icon_matching = [ - ("up", "mdi:arrow-up-circle"), - ("down", "mdi:arrow-down-circle"), - ("fav", "mdi:heart-circle"), - ("open", "mdi:arrow-up-circle"), - ("close", "mdi:arrow-down-circle"), +query_translation_key_matching = [ + ("up", "up"), + ("down", "down"), + ("fav", "fav"), + ("open", "open"), + ("close", "close"), + ("unknown_action", None), ] @@ -56,16 +57,28 @@ async def test_tvliftbox_init( assert state.name == "My tvLiftBox tvLiftBox-open_or_stop" -@pytest.mark.parametrize("input", query_icon_matching) -async def test_get_icon( - input, tvliftbox, hass: HomeAssistant, caplog: pytest.LogCaptureFixture +@pytest.mark.parametrize( + ("query_string", "expected_translation_key"), + query_translation_key_matching, + ids=[q[0] for q in query_translation_key_matching], +) +async def test_button_translation_key( + query_string: str, + expected_translation_key: str | None, + tvliftbox: tuple[blebox_uniapi.button.Button, str], + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, ) -> None: - """Test if proper icon is returned.""" + """Test that the correct translation_key is assigned based on query_string.""" caplog.set_level(logging.ERROR) feature_mock, entity_id = tvliftbox - feature_mock.query_string = input[0] - _ = await async_setup_entity(hass, entity_id) + feature_mock.query_string = query_string + await async_setup_entity(hass, entity_id) + state = hass.states.get(entity_id) + assert state is not None - assert state.attributes[ATTR_ICON] == input[1] + entity = er.async_get(hass).async_get(entity_id) + assert entity is not None + assert entity.translation_key == expected_translation_key diff --git a/tests/components/blebox/test_config_flow.py b/tests/components/blebox/test_config_flow.py index a4823b81d97956..56fde37206651c 100644 --- a/tests/components/blebox/test_config_flow.py +++ b/tests/components/blebox/test_config_flow.py @@ -9,6 +9,7 @@ from homeassistant import config_entries from homeassistant.components.blebox import config_flow +from homeassistant.components.blebox.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -70,14 +71,14 @@ async def test_flow_works( """Test that config flow works.""" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, + DOMAIN, context={"source": config_entries.SOURCE_USER}, data={config_flow.CONF_HOST: "172.2.3.4", config_flow.CONF_PORT: 80}, ) @@ -107,7 +108,7 @@ async def test_flow_with_connection_failure( ) result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, + DOMAIN, context={"source": config_entries.SOURCE_USER}, data={config_flow.CONF_HOST: "172.2.3.4", config_flow.CONF_PORT: 80}, ) @@ -122,7 +123,7 @@ async def test_flow_with_api_failure(hass: HomeAssistant, product_class_mock) -> ) result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, + DOMAIN, context={"source": config_entries.SOURCE_USER}, data={config_flow.CONF_HOST: "172.2.3.4", config_flow.CONF_PORT: 80}, ) @@ -136,7 +137,7 @@ async def test_flow_with_unknown_failure( with product_class_mock as products_class: products_class.async_from_host = AsyncMock(side_effect=RuntimeError) result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, + DOMAIN, context={"source": config_entries.SOURCE_USER}, data={config_flow.CONF_HOST: "172.2.3.4", config_flow.CONF_PORT: 80}, ) @@ -153,7 +154,7 @@ async def test_flow_with_unsupported_version( ) result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, + DOMAIN, context={"source": config_entries.SOURCE_USER}, data={config_flow.CONF_HOST: "172.2.3.4", config_flow.CONF_PORT: 80}, ) @@ -168,7 +169,7 @@ async def test_flow_with_auth_failure(hass: HomeAssistant, product_class_mock) - ) result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, + DOMAIN, context={"source": config_entries.SOURCE_USER}, data={config_flow.CONF_HOST: "172.2.3.4", config_flow.CONF_PORT: 80}, ) @@ -191,7 +192,7 @@ async def test_already_configured(hass: HomeAssistant, valid_feature_mock) -> No await hass.async_block_till_done() result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, + DOMAIN, context={"source": config_entries.SOURCE_USER}, data={config_flow.CONF_HOST: "172.2.3.4", config_flow.CONF_PORT: 80}, ) @@ -227,7 +228,7 @@ async def test_async_remove_entry( async def test_flow_with_zeroconf(hass: HomeAssistant) -> None: """Test setup from zeroconf discovery.""" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=ZeroconfServiceInfo( ip_address=ip_address("172.100.123.4"), @@ -264,7 +265,7 @@ async def test_flow_with_zeroconf_when_already_configured( return_value=feature.product, ): result2 = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=ZeroconfServiceInfo( ip_address=ip_address("172.100.123.4"), @@ -288,7 +289,7 @@ async def test_flow_with_zeroconf_when_device_unsupported(hass: HomeAssistant) - side_effect=blebox_uniapi.error.UnsupportedBoxVersion, ): result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=ZeroconfServiceInfo( ip_address=ip_address("172.100.123.4"), @@ -314,7 +315,7 @@ async def test_flow_with_zeroconf_when_device_response_unsupported( side_effect=blebox_uniapi.error.UnsupportedBoxResponse, ): result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=ZeroconfServiceInfo( ip_address=ip_address("172.100.123.4"), diff --git a/tests/components/blebox/test_light.py b/tests/components/blebox/test_light.py index 521cfaa7db1a08..99e4518a939786 100644 --- a/tests/components/blebox/test_light.py +++ b/tests/components/blebox/test_light.py @@ -24,6 +24,7 @@ STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr from .conftest import async_setup_config_entry, async_setup_entity, mock_feature @@ -602,7 +603,7 @@ async def test_turn_on_failure( await async_setup_entity(hass, entity_id) feature_mock.sensible_on_value = 123 - with pytest.raises(ValueError) as info: + with pytest.raises(HomeAssistantError) as info: await hass.services.async_call( "light", SERVICE_TURN_ON, @@ -610,9 +611,7 @@ async def test_turn_on_failure( blocking=True, ) - assert f"Turning on '{feature_mock.full_name}' failed: Bad value 123" in str( - info.value - ) + assert info.value.translation_key == "bad_value" async def test_wlightbox_on_effect(wlightbox, hass: HomeAssistant) -> None: @@ -632,7 +631,7 @@ def turn_on(value): feature_mock.async_on = AsyncMock(side_effect=turn_on) - with pytest.raises(ValueError) as info: + with pytest.raises(HomeAssistantError) as info: await hass.services.async_call( "light", SERVICE_TURN_ON, @@ -640,10 +639,7 @@ def turn_on(value): blocking=True, ) - assert ( - f"Turning on with effect '{feature_mock.full_name}' failed: " - "NOT IN LIST not in effect list." - ) in str(info.value) + assert info.value.translation_key == "effect_not_found" await hass.services.async_call( "light", diff --git a/tests/components/cast/test_config_flow.py b/tests/components/cast/test_config_flow.py index 5494430898db70..19d14fa99b6f4c 100644 --- a/tests/components/cast/test_config_flow.py +++ b/tests/components/cast/test_config_flow.py @@ -66,7 +66,7 @@ async def test_single_instance(hass: HomeAssistant, source) -> None: await hass.async_block_till_done() result = await hass.config_entries.flow.async_init( - "cast", context={"source": source} + DOMAIN, context={"source": source} ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -75,7 +75,7 @@ async def test_single_instance(hass: HomeAssistant, source) -> None: async def test_user_setup(hass: HomeAssistant) -> None: """Test we can finish a config flow.""" result = await hass.config_entries.flow.async_init( - "cast", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM @@ -95,7 +95,7 @@ async def test_user_setup(hass: HomeAssistant) -> None: async def test_user_setup_options(hass: HomeAssistant) -> None: """Test we can finish a config flow.""" result = await hass.config_entries.flow.async_init( - "cast", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM @@ -117,7 +117,7 @@ async def test_user_setup_options(hass: HomeAssistant) -> None: async def test_zeroconf_setup(hass: HomeAssistant) -> None: """Test we can finish a config flow through zeroconf.""" result = await hass.config_entries.flow.async_init( - "cast", context={"source": config_entries.SOURCE_ZEROCONF} + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF} ) assert result["type"] is FlowResultType.FORM @@ -140,7 +140,7 @@ async def test_zeroconf_setup_onboarding(hass: HomeAssistant) -> None: "homeassistant.components.onboarding.async_is_onboarded", return_value=False ): result = await hass.config_entries.flow.async_init( - "cast", context={"source": config_entries.SOURCE_ZEROCONF} + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF} ) users = await hass.auth.async_get_users() @@ -271,7 +271,7 @@ async def test_option_flow( async def test_known_hosts(hass: HomeAssistant, castbrowser_mock) -> None: """Test known hosts is passed to pychromecast.""" result = await hass.config_entries.flow.async_init( - "cast", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( result["flow_id"], {"known_hosts": ["192.168.0.1", "192.168.0.2"]} diff --git a/tests/components/cast/test_home_assistant_cast.py b/tests/components/cast/test_home_assistant_cast.py index 453b6a7066eef3..40176aa376deb9 100644 --- a/tests/components/cast/test_home_assistant_cast.py +++ b/tests/components/cast/test_home_assistant_cast.py @@ -23,7 +23,7 @@ async def test_service_show_view(hass: HomeAssistant) -> None: # No valid URL with pytest.raises(HomeAssistantError): await hass.services.async_call( - "cast", + DOMAIN, "show_lovelace_view", {"entity_id": "media_player.kitchen", "view_path": "mock_path"}, blocking=True, @@ -35,7 +35,7 @@ async def test_service_show_view(hass: HomeAssistant) -> None: {"external_url": "https://example.com"}, ) await hass.services.async_call( - "cast", + DOMAIN, "show_lovelace_view", {"entity_id": "media_player.kitchen", "view_path": "mock_path"}, blocking=True, @@ -65,7 +65,7 @@ async def test_service_show_view_dashboard(hass: HomeAssistant) -> None: calls = async_mock_signal(hass, home_assistant_cast.SIGNAL_HASS_CAST_SHOW_VIEW) await hass.services.async_call( - "cast", + DOMAIN, "show_lovelace_view", { "entity_id": "media_player.kitchen", @@ -101,7 +101,7 @@ async def test_use_cloud_url(hass: HomeAssistant) -> None: return_value="https://something.nabu.casa", ): await hass.services.async_call( - "cast", + DOMAIN, "show_lovelace_view", {"entity_id": "media_player.kitchen", "view_path": "mock_path"}, blocking=True, diff --git a/tests/components/climate/test_device_action.py b/tests/components/climate/test_device_action.py index a8165ae1b68813..131fcb26812574 100644 --- a/tests/components/climate/test_device_action.py +++ b/tests/components/climate/test_device_action.py @@ -199,8 +199,8 @@ async def test_action( }, ) - set_hvac_mode_calls = async_mock_service(hass, "climate", "set_hvac_mode") - set_preset_mode_calls = async_mock_service(hass, "climate", "set_preset_mode") + set_hvac_mode_calls = async_mock_service(hass, DOMAIN, "set_hvac_mode") + set_preset_mode_calls = async_mock_service(hass, DOMAIN, "set_preset_mode") hass.bus.async_fire("test_event_set_hvac_mode") await hass.async_block_till_done() @@ -273,7 +273,7 @@ async def test_action_legacy( }, ) - set_hvac_mode_calls = async_mock_service(hass, "climate", "set_hvac_mode") + set_hvac_mode_calls = async_mock_service(hass, DOMAIN, "set_hvac_mode") hass.bus.async_fire("test_event_set_hvac_mode") await hass.async_block_till_done() diff --git a/tests/components/counter/test_reproduce_state.py b/tests/components/counter/test_reproduce_state.py index 420c9b8907df90..6b985b5a6872c1 100644 --- a/tests/components/counter/test_reproduce_state.py +++ b/tests/components/counter/test_reproduce_state.py @@ -2,6 +2,7 @@ import pytest +from homeassistant.components.counter import DOMAIN from homeassistant.core import HomeAssistant, State from homeassistant.helpers.state import async_reproduce_state @@ -19,7 +20,7 @@ async def test_reproducing_states( {"minimum": 5, "maximum": 15, "step": 3}, ) - configure_calls = async_mock_service(hass, "counter", "set_value") + configure_calls = async_mock_service(hass, DOMAIN, "set_value") # These calls should do nothing as entities already in desired state await async_reproduce_state( diff --git a/tests/components/cover/test_device_action.py b/tests/components/cover/test_device_action.py index 435711db8fe171..79aae1aef650b7 100644 --- a/tests/components/cover/test_device_action.py +++ b/tests/components/cover/test_device_action.py @@ -390,9 +390,9 @@ async def test_action( ) await hass.async_block_till_done() - open_calls = async_mock_service(hass, "cover", "open_cover") - close_calls = async_mock_service(hass, "cover", "close_cover") - stop_calls = async_mock_service(hass, "cover", "stop_cover") + open_calls = async_mock_service(hass, DOMAIN, "open_cover") + close_calls = async_mock_service(hass, DOMAIN, "close_cover") + stop_calls = async_mock_service(hass, DOMAIN, "stop_cover") hass.bus.async_fire("test_event_open") await hass.async_block_till_done() @@ -459,7 +459,7 @@ async def test_action_legacy( ) await hass.async_block_till_done() - open_calls = async_mock_service(hass, "cover", "open_cover") + open_calls = async_mock_service(hass, DOMAIN, "open_cover") hass.bus.async_fire("test_event_open") await hass.async_block_till_done() @@ -515,8 +515,8 @@ async def test_action_tilt( ) await hass.async_block_till_done() - open_calls = async_mock_service(hass, "cover", "open_cover_tilt") - close_calls = async_mock_service(hass, "cover", "close_cover_tilt") + open_calls = async_mock_service(hass, DOMAIN, "open_cover_tilt") + close_calls = async_mock_service(hass, DOMAIN, "close_cover_tilt") hass.bus.async_fire("test_event_open") await hass.async_block_till_done() @@ -594,8 +594,8 @@ async def test_action_set_position( ) await hass.async_block_till_done() - cover_pos_calls = async_mock_service(hass, "cover", "set_cover_position") - tilt_pos_calls = async_mock_service(hass, "cover", "set_cover_tilt_position") + cover_pos_calls = async_mock_service(hass, DOMAIN, "set_cover_position") + tilt_pos_calls = async_mock_service(hass, DOMAIN, "set_cover_tilt_position") hass.bus.async_fire("test_event_set_pos") await hass.async_block_till_done() diff --git a/tests/components/cover/test_reproduce_state.py b/tests/components/cover/test_reproduce_state.py index dfc22abac91e69..c3d1d693dd51dc 100644 --- a/tests/components/cover/test_reproduce_state.py +++ b/tests/components/cover/test_reproduce_state.py @@ -7,6 +7,7 @@ ATTR_CURRENT_TILT_POSITION, ATTR_POSITION, ATTR_TILT_POSITION, + DOMAIN, CoverEntityFeature, CoverState, ) @@ -252,13 +253,13 @@ async def test_reproducing_states( ATTR_SUPPORTED_FEATURES: CoverEntityFeature.SET_TILT_POSITION, }, ) - close_calls = async_mock_service(hass, "cover", SERVICE_CLOSE_COVER) - open_calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER) - close_tilt_calls = async_mock_service(hass, "cover", SERVICE_CLOSE_COVER_TILT) - open_tilt_calls = async_mock_service(hass, "cover", SERVICE_OPEN_COVER_TILT) - position_calls = async_mock_service(hass, "cover", SERVICE_SET_COVER_POSITION) + close_calls = async_mock_service(hass, DOMAIN, SERVICE_CLOSE_COVER) + open_calls = async_mock_service(hass, DOMAIN, SERVICE_OPEN_COVER) + close_tilt_calls = async_mock_service(hass, DOMAIN, SERVICE_CLOSE_COVER_TILT) + open_tilt_calls = async_mock_service(hass, DOMAIN, SERVICE_OPEN_COVER_TILT) + position_calls = async_mock_service(hass, DOMAIN, SERVICE_SET_COVER_POSITION) position_tilt_calls = async_mock_service( - hass, "cover", SERVICE_SET_COVER_TILT_POSITION + hass, DOMAIN, SERVICE_SET_COVER_TILT_POSITION ) # These calls should do nothing as entities already in desired state diff --git a/tests/components/daikin/test_config_flow.py b/tests/components/daikin/test_config_flow.py index 4333050388caa6..906875de617a36 100644 --- a/tests/components/daikin/test_config_flow.py +++ b/tests/components/daikin/test_config_flow.py @@ -8,7 +8,7 @@ from pydaikin.exceptions import DaikinException import pytest -from homeassistant.components.daikin.const import KEY_MAC +from homeassistant.components.daikin.const import DOMAIN, KEY_MAC from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PASSWORD from homeassistant.core import HomeAssistant @@ -59,7 +59,7 @@ def mock_daikin_discovery(): async def test_user(hass: HomeAssistant, mock_daikin) -> None: """Test user config.""" result = await hass.config_entries.flow.async_init( - "daikin", + DOMAIN, context={"source": SOURCE_USER}, ) @@ -67,7 +67,7 @@ async def test_user(hass: HomeAssistant, mock_daikin) -> None: assert result["step_id"] == "user" result = await hass.config_entries.flow.async_init( - "daikin", + DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: HOST}, ) @@ -81,7 +81,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant, mock_daikin) -> None: """Test we abort if Daikin is already setup.""" MockConfigEntry(domain="daikin", unique_id=MAC).add_to_hass(hass) result = await hass.config_entries.flow.async_init( - "daikin", + DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: HOST, KEY_MAC: MAC}, ) @@ -105,7 +105,7 @@ async def test_device_abort(hass: HomeAssistant, mock_daikin, s_effect, reason) mock_daikin.side_effect = s_effect result = await hass.config_entries.flow.async_init( - "daikin", + DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: HOST, KEY_MAC: MAC}, ) @@ -117,7 +117,7 @@ async def test_device_abort(hass: HomeAssistant, mock_daikin, s_effect, reason) async def test_api_password_abort(hass: HomeAssistant) -> None: """Test device abort.""" result = await hass.config_entries.flow.async_init( - "daikin", + DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: HOST, CONF_API_KEY: "aa", CONF_PASSWORD: "aa"}, ) @@ -149,7 +149,7 @@ async def test_discovery_zeroconf( ) -> None: """Test discovery/zeroconf step.""" result = await hass.config_entries.flow.async_init( - "daikin", + DOMAIN, context={"source": source}, data=data, ) @@ -158,7 +158,7 @@ async def test_discovery_zeroconf( MockConfigEntry(domain="daikin", unique_id=unique_id).add_to_hass(hass) result = await hass.config_entries.flow.async_init( - "daikin", + DOMAIN, context={"source": SOURCE_USER, "unique_id": unique_id}, data={CONF_HOST: HOST}, ) @@ -167,7 +167,7 @@ async def test_discovery_zeroconf( assert result["reason"] == "already_configured" result = await hass.config_entries.flow.async_init( - "daikin", + DOMAIN, context={"source": source}, data=data, ) diff --git a/tests/components/dialogflow/test_init.py b/tests/components/dialogflow/test_init.py index 8144bef7c1c884..2bf2a462df6220 100644 --- a/tests/components/dialogflow/test_init.py +++ b/tests/components/dialogflow/test_init.py @@ -8,6 +8,7 @@ from homeassistant import config_entries from homeassistant.components import dialogflow, intent_script +from homeassistant.components.dialogflow import DOMAIN from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.core_config import async_process_ha_core_config from homeassistant.data_entry_flow import FlowResultType @@ -89,7 +90,7 @@ async def fixture(hass: HomeAssistant, hass_client_no_auth: ClientSessionGenerat ) result = await hass.config_entries.flow.async_init( - "dialogflow", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM, result diff --git a/tests/components/drop_connect/test_config_flow.py b/tests/components/drop_connect/test_config_flow.py index 8cd765b46b448a..2f6afb42f4d86a 100644 --- a/tests/components/drop_connect/test_config_flow.py +++ b/tests/components/drop_connect/test_config_flow.py @@ -1,6 +1,7 @@ """Test config flow.""" from homeassistant import config_entries +from homeassistant.components.drop_connect.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.service_info.mqtt import MqttServiceInfo @@ -19,7 +20,7 @@ async def test_mqtt_setup(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> N timestamp=None, ) result = await hass.config_entries.flow.async_init( - "drop_connect", + DOMAIN, context={"source": config_entries.SOURCE_MQTT}, data=discovery_info, ) @@ -56,7 +57,7 @@ async def test_duplicate(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> No timestamp=None, ) result = await hass.config_entries.flow.async_init( - "drop_connect", + DOMAIN, context={"source": config_entries.SOURCE_MQTT}, data=discovery_info, ) @@ -73,7 +74,7 @@ async def test_duplicate(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> No # Attempting configuration of the same object should abort result = await hass.config_entries.flow.async_init( - "drop_connect", + DOMAIN, context={"source": config_entries.SOURCE_MQTT}, data=discovery_info, ) @@ -95,7 +96,7 @@ async def test_mqtt_setup_incomplete_payload( timestamp=None, ) result = await hass.config_entries.flow.async_init( - "drop_connect", + DOMAIN, context={"source": config_entries.SOURCE_MQTT}, data=discovery_info, ) @@ -117,7 +118,7 @@ async def test_mqtt_setup_bad_json( timestamp=None, ) result = await hass.config_entries.flow.async_init( - "drop_connect", + DOMAIN, context={"source": config_entries.SOURCE_MQTT}, data=discovery_info, ) @@ -139,7 +140,7 @@ async def test_mqtt_setup_bad_topic( timestamp=None, ) result = await hass.config_entries.flow.async_init( - "drop_connect", + DOMAIN, context={"source": config_entries.SOURCE_MQTT}, data=discovery_info, ) @@ -161,7 +162,7 @@ async def test_mqtt_setup_no_payload( timestamp=None, ) result = await hass.config_entries.flow.async_init( - "drop_connect", + DOMAIN, context={"source": config_entries.SOURCE_MQTT}, data=discovery_info, ) @@ -173,7 +174,7 @@ async def test_mqtt_setup_no_payload( async def test_user_setup(hass: HomeAssistant) -> None: """Test user setup.""" result = await hass.config_entries.flow.async_init( - "drop_connect", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" diff --git a/tests/components/envisalink/__init__.py b/tests/components/envisalink/__init__.py new file mode 100644 index 00000000000000..60bc69230a7188 --- /dev/null +++ b/tests/components/envisalink/__init__.py @@ -0,0 +1 @@ +"""Tests for the Envisalink integration.""" diff --git a/tests/components/envisalink/conftest.py b/tests/components/envisalink/conftest.py new file mode 100644 index 00000000000000..33ae8721e91e2e --- /dev/null +++ b/tests/components/envisalink/conftest.py @@ -0,0 +1,66 @@ +"""Fixtures for Envisalink tests.""" + +from collections.abc import Generator +from unittest.mock import MagicMock, patch + +from pyenvisalink.alarm_state import AlarmState +import pytest + +from homeassistant.components.envisalink import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers.typing import ConfigType +from homeassistant.setup import async_setup_component + +# The configured alarm code, shared with the tests so the service-call +# assertions stay in sync with the code HA injects as the default. +MOCK_CODE = "1234" + +# Mirrors the legacy YAML config keys (CONF_USERNAME == "user_name", CONF_PASS +# == "password"). The integration is YAML-only (no config entry), so a future +# config-entry migration replaces this whole fixture. +MOCK_CONFIG = { + DOMAIN: { + "host": "1.2.3.4", + "panel_type": "DSC", + "user_name": "user", + "password": "pass", + "code": MOCK_CODE, + "partitions": {1: {"name": "Main Home"}}, + "zones": {1: {"name": "Front Door", "type": "door"}}, + } +} + +# Entity ids derived from the configured names. +ALARM_ENTITY = "alarm_control_panel.main_home" +KEYPAD_ENTITY = "sensor.main_home_keypad" +ZONE_ENTITY = "binary_sensor.front_door" + + +@pytest.fixture +def mock_controller() -> Generator[MagicMock]: + """Patch EnvisalinkAlarmPanel with an autospec'd mock controller. + + The integration waits on a connection future that is resolved by the + login-success callback, so the mock's start() invokes it. Tests can + reassign ``controller.start.side_effect`` to ``callback_login_failure`` or + ``callback_login_timeout`` to exercise alternate connection outcomes. + """ + with patch( + "homeassistant.components.envisalink.EnvisalinkAlarmPanel", autospec=True + ) as mock_panel: + controller = mock_panel.return_value + # (max_zones=64, max_partitions=8) — sized to the panel's hardware limits. + controller.alarm_state = AlarmState.get_initial_alarm_state(64, 8) + # A non-empty alpha makes the initial partition state DISARMED (not None). + controller.alarm_state["partition"][1]["status"]["alpha"] = "Ready" + controller.start.side_effect = lambda: controller.callback_login_success(None) + yield controller + + +async def setup_envisalink( + hass: HomeAssistant, config: ConfigType | None = None +) -> bool: + """Set up the envisalink component and wait for it to finish.""" + result = await async_setup_component(hass, DOMAIN, config or MOCK_CONFIG) + await hass.async_block_till_done() + return result diff --git a/tests/components/envisalink/test_alarm_control_panel.py b/tests/components/envisalink/test_alarm_control_panel.py new file mode 100644 index 00000000000000..4e4de7940659fb --- /dev/null +++ b/tests/components/envisalink/test_alarm_control_panel.py @@ -0,0 +1,164 @@ +"""Tests for the Envisalink alarm control panel.""" + +from copy import deepcopy +from unittest.mock import MagicMock + +import pytest + +from homeassistant.components.alarm_control_panel import ( + DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, + AlarmControlPanelState, + CodeFormat, +) +from homeassistant.const import ( + SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_NIGHT, + SERVICE_ALARM_DISARM, + SERVICE_ALARM_TRIGGER, + STATE_UNKNOWN, +) +from homeassistant.core import HomeAssistant + +from .conftest import ALARM_ENTITY, DOMAIN, MOCK_CODE, MOCK_CONFIG, setup_envisalink + + +@pytest.mark.parametrize( + ("status_overrides", "expected_state"), + [ + ({"alarm": True}, AlarmControlPanelState.TRIGGERED), + ({"armed_zero_entry_delay": True}, AlarmControlPanelState.ARMED_NIGHT), + ({"armed_away": True}, AlarmControlPanelState.ARMED_AWAY), + ({"armed_stay": True}, AlarmControlPanelState.ARMED_HOME), + ({"exit_delay": True}, AlarmControlPanelState.ARMING), + ({"entry_delay": True}, AlarmControlPanelState.PENDING), + ({}, AlarmControlPanelState.DISARMED), + ({"alpha": ""}, STATE_UNKNOWN), + ], +) +async def test_alarm_states( + hass: HomeAssistant, + mock_controller: MagicMock, + status_overrides: dict[str, bool | str], + expected_state: str, +) -> None: + """Test the partition status maps to the correct alarm state.""" + assert await setup_envisalink(hass) + + status = mock_controller.alarm_state["partition"][1]["status"] + status.update(status_overrides) + mock_controller.callback_partition_state_change(1) + await hass.async_block_till_done() + + assert hass.states.get(ALARM_ENTITY).state == expected_state + + +@pytest.mark.parametrize( + ("service", "method"), + [ + (SERVICE_ALARM_ARM_HOME, "arm_stay_partition"), + (SERVICE_ALARM_ARM_AWAY, "arm_away_partition"), + (SERVICE_ALARM_ARM_NIGHT, "arm_night_partition"), + (SERVICE_ALARM_DISARM, "disarm_partition"), + ], +) +async def test_arm_disarm_commands( + hass: HomeAssistant, + mock_controller: MagicMock, + service: str, + method: str, +) -> None: + """Test arm/disarm services call the controller with code and partition. + + No code is supplied in the call: the keypad is hidden when a code is + configured, so HA injects the configured default code. This proves the + default-code path, not just an echo of a code we passed in. + """ + assert await setup_envisalink(hass) + + await hass.services.async_call( + ALARM_CONTROL_PANEL_DOMAIN, + service, + {"entity_id": ALARM_ENTITY}, + blocking=True, + ) + + getattr(mock_controller, method).assert_called_once_with(MOCK_CODE, 1) + + +async def test_trigger_calls_panic( + hass: HomeAssistant, mock_controller: MagicMock +) -> None: + """Test the trigger service fires a panic alarm of the configured type.""" + assert await setup_envisalink(hass) + + await hass.services.async_call( + ALARM_CONTROL_PANEL_DOMAIN, + SERVICE_ALARM_TRIGGER, + {"entity_id": ALARM_ENTITY}, + blocking=True, + ) + + # "Police" is the default panic type when none is configured. + mock_controller.panic_alarm.assert_called_once_with("Police") + + +async def test_code_format_with_configured_code( + hass: HomeAssistant, mock_controller: MagicMock +) -> None: + """Test the keypad is hidden (no code_format) when a code is configured.""" + assert await setup_envisalink(hass) + + assert hass.states.get(ALARM_ENTITY).attributes.get("code_format") is None + + +async def test_code_format_without_configured_code( + hass: HomeAssistant, mock_controller: MagicMock +) -> None: + """Test the numeric keypad shows when no code is configured.""" + config = deepcopy(MOCK_CONFIG) + del config[DOMAIN]["code"] + assert await setup_envisalink(hass, config) + + assert hass.states.get(ALARM_ENTITY).attributes["code_format"] == CodeFormat.NUMBER + + +async def test_partition_update_filtering( + hass: HomeAssistant, mock_controller: MagicMock +) -> None: + """Test partition updates are filtered by number; None applies to all.""" + assert await setup_envisalink(hass) + status = mock_controller.alarm_state["partition"][1]["status"] + status["alarm"] = True + + # Update targeting a different partition is ignored (stays disarmed). + mock_controller.callback_partition_state_change(2) + await hass.async_block_till_done() + assert hass.states.get(ALARM_ENTITY).state == AlarmControlPanelState.DISARMED + + # A matching partition delivered as a string is coerced and applies. + mock_controller.callback_partition_state_change("1") + await hass.async_block_till_done() + assert hass.states.get(ALARM_ENTITY).state == AlarmControlPanelState.TRIGGERED + + # A None partition applies to every entity. + status["alarm"] = False + mock_controller.callback_partition_state_change(None) + await hass.async_block_till_done() + assert hass.states.get(ALARM_ENTITY).state == AlarmControlPanelState.DISARMED + + +async def test_alarm_keypress_service( + hass: HomeAssistant, mock_controller: MagicMock +) -> None: + """Test the custom alarm_keypress service forwards to the partition.""" + assert await setup_envisalink(hass) + + await hass.services.async_call( + DOMAIN, + "alarm_keypress", + {"entity_id": ALARM_ENTITY, "keypress": "*71"}, + blocking=True, + ) + + mock_controller.keypresses_to_partition.assert_called_once_with(1, "*71") diff --git a/tests/components/envisalink/test_binary_sensor.py b/tests/components/envisalink/test_binary_sensor.py new file mode 100644 index 00000000000000..e29a7f29b5a73a --- /dev/null +++ b/tests/components/envisalink/test_binary_sensor.py @@ -0,0 +1,86 @@ +"""Tests for the Envisalink zone binary sensors.""" + +from datetime import timedelta +from unittest.mock import MagicMock + +from freezegun.api import FrozenDateTimeFactory + +from homeassistant.components.binary_sensor import BinarySensorDeviceClass +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_LAST_TRIP_TIME, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import HomeAssistant +from homeassistant.util import dt as dt_util + +from .conftest import ZONE_ENTITY, setup_envisalink + +# The library reports last_fault in seconds, capped at 327680 (65536 five-second +# ticks); at or above the cap the real value is unknown. +LAST_FAULT_MAX_SECONDS = 65536 * 5 + + +async def test_zone_is_on(hass: HomeAssistant, mock_controller: MagicMock) -> None: + """Test the zone reflects its open status on a zone update.""" + assert await setup_envisalink(hass) + assert hass.states.get(ZONE_ENTITY).state == STATE_OFF + + mock_controller.alarm_state["zone"][1]["status"]["open"] = True + mock_controller.callback_zone_state_change(1) + await hass.async_block_till_done() + + assert hass.states.get(ZONE_ENTITY).state == STATE_ON + + +async def test_zone_attributes( + hass: HomeAssistant, mock_controller: MagicMock, freezer: FrozenDateTimeFactory +) -> None: + """Test the zone exposes device class, zone number, and last trip time.""" + freezer.move_to("2026-01-01 12:00:00+00:00") + # last_fault is the number of seconds since the fault; it is subtracted + # from the current (second-accurate) time to get the last trip time. + mock_controller.alarm_state["zone"][1]["last_fault"] = 300 + assert await setup_envisalink(hass) + + attrs = hass.states.get(ZONE_ENTITY).attributes + assert attrs[ATTR_DEVICE_CLASS] == BinarySensorDeviceClass.DOOR + assert attrs["zone"] == 1 + expected = dt_util.now().replace(microsecond=0) - timedelta(seconds=300) + assert attrs[ATTR_LAST_TRIP_TIME] == expected.isoformat() + + +async def test_zone_last_trip_time_unknown_at_max( + hass: HomeAssistant, mock_controller: MagicMock +) -> None: + """Test last trip time is None once the library caps the fault counter.""" + mock_controller.alarm_state["zone"][1]["last_fault"] = LAST_FAULT_MAX_SECONDS + assert await setup_envisalink(hass) + + assert hass.states.get(ZONE_ENTITY).attributes[ATTR_LAST_TRIP_TIME] is None + + +async def test_zone_update_filtering( + hass: HomeAssistant, mock_controller: MagicMock +) -> None: + """Test zone updates are filtered by number; None applies to all.""" + assert await setup_envisalink(hass) + zone_status = mock_controller.alarm_state["zone"][1]["status"] + zone_status["open"] = True + + # Update for a different zone (int) is ignored. + mock_controller.callback_zone_state_change(2) + await hass.async_block_till_done() + assert hass.states.get(ZONE_ENTITY).state == STATE_OFF + + # A matching zone delivered as a string is coerced and applies. + mock_controller.callback_zone_state_change("1") + await hass.async_block_till_done() + assert hass.states.get(ZONE_ENTITY).state == STATE_ON + + # A None zone applies to every entity. + zone_status["open"] = False + mock_controller.callback_zone_state_change(None) + await hass.async_block_till_done() + assert hass.states.get(ZONE_ENTITY).state == STATE_OFF diff --git a/tests/components/envisalink/test_init.py b/tests/components/envisalink/test_init.py new file mode 100644 index 00000000000000..113871b565b05d --- /dev/null +++ b/tests/components/envisalink/test_init.py @@ -0,0 +1,116 @@ +"""Tests for the Envisalink setup.""" + +from collections.abc import Awaitable, Callable +from unittest.mock import MagicMock + +import pytest + +from homeassistant.components.envisalink.alarm_control_panel import ( + async_setup_platform as alarm_setup_platform, +) +from homeassistant.components.envisalink.binary_sensor import ( + async_setup_platform as binary_sensor_setup_platform, +) +from homeassistant.components.envisalink.sensor import ( + async_setup_platform as sensor_setup_platform, +) +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType + +from .conftest import ( + ALARM_ENTITY, + DOMAIN, + KEYPAD_ENTITY, + MOCK_CODE, + ZONE_ENTITY, + setup_envisalink, +) + +SetupPlatform = Callable[ + [HomeAssistant, ConfigType, AddEntitiesCallback, DiscoveryInfoType | None], + Awaitable[None], +] + + +async def test_setup_creates_entities( + hass: HomeAssistant, mock_controller: MagicMock +) -> None: + """Test setup creates the alarm, keypad and zone entities.""" + assert await setup_envisalink(hass) + + assert hass.states.get(ALARM_ENTITY) is not None + assert hass.states.get(KEYPAD_ENTITY) is not None + assert hass.states.get(ZONE_ENTITY) is not None + + +async def test_setup_fails_on_login_failure( + hass: HomeAssistant, mock_controller: MagicMock +) -> None: + """Test setup returns False when the Envisalink rejects the login.""" + mock_controller.start.side_effect = lambda: mock_controller.callback_login_failure( + None + ) + + assert await setup_envisalink(hass) is False + assert hass.states.get(ALARM_ENTITY) is None + + +async def test_setup_succeeds_on_connection_timeout( + hass: HomeAssistant, mock_controller: MagicMock +) -> None: + """Test setup proceeds (retry mode) when the first connection times out.""" + mock_controller.start.side_effect = lambda: mock_controller.callback_login_timeout( + None + ) + + assert await setup_envisalink(hass) + assert hass.states.get(ALARM_ENTITY) is not None + + +async def test_controller_stopped_on_shutdown( + hass: HomeAssistant, mock_controller: MagicMock +) -> None: + """Test the controller connection is stopped on Home Assistant shutdown.""" + assert await setup_envisalink(hass) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + mock_controller.stop.assert_called_once() + + +@pytest.mark.parametrize( + "setup_platform", + [ + alarm_setup_platform, + binary_sensor_setup_platform, + sensor_setup_platform, + ], +) +async def test_platform_setup_without_discovery_is_noop( + hass: HomeAssistant, setup_platform: SetupPlatform +) -> None: + """Test a platform set up directly (no discovery info) adds no entities.""" + add_entities = MagicMock() + await setup_platform(hass, {}, add_entities, None) + add_entities.assert_not_called() + + +async def test_invoke_custom_function_service( + hass: HomeAssistant, mock_controller: MagicMock +) -> None: + """Test the PGM service forwards the code, partition and function.""" + assert await setup_envisalink(hass) + + # Distinct pgm/partition values so the assertion pins each positional arg + # of command_output(code, partition, custom_function). + await hass.services.async_call( + DOMAIN, + "invoke_custom_function", + {"pgm": "7", "partition": "2"}, + blocking=True, + ) + + mock_controller.command_output.assert_called_once_with(MOCK_CODE, "2", "7") diff --git a/tests/components/envisalink/test_sensor.py b/tests/components/envisalink/test_sensor.py new file mode 100644 index 00000000000000..28fc6759be94b0 --- /dev/null +++ b/tests/components/envisalink/test_sensor.py @@ -0,0 +1,33 @@ +"""Tests for the Envisalink keypad sensor.""" + +from unittest.mock import MagicMock + +from homeassistant.core import HomeAssistant + +from .conftest import KEYPAD_ENTITY, setup_envisalink + + +async def test_keypad_native_value( + hass: HomeAssistant, mock_controller: MagicMock +) -> None: + """Test the keypad sensor shows the partition alpha text.""" + assert await setup_envisalink(hass) + assert hass.states.get(KEYPAD_ENTITY).state == "Ready" + + mock_controller.alarm_state["partition"][1]["status"]["alpha"] = "Armed Away" + # A matching partition delivered as a string is coerced and applies. + mock_controller.callback_keypad_update("1") + await hass.async_block_till_done() + + assert hass.states.get(KEYPAD_ENTITY).state == "Armed Away" + + +async def test_keypad_attributes_expose_status( + hass: HomeAssistant, mock_controller: MagicMock +) -> None: + """Test the keypad sensor exposes the full partition status as attributes.""" + assert await setup_envisalink(hass) + + attrs = hass.states.get(KEYPAD_ENTITY).attributes + assert attrs["armed_away"] is False + assert attrs["alpha"] == "Ready" diff --git a/tests/components/esphome/test_manager.py b/tests/components/esphome/test_manager.py index e6913e5e422842..89a42e9aadf9c8 100644 --- a/tests/components/esphome/test_manager.py +++ b/tests/components/esphome/test_manager.py @@ -157,7 +157,7 @@ async def test_esphome_device_service_calls_not_allowed( device_info={"esphome_version": "2023.3.0"}, ) await hass.async_block_till_done() - mock_esphome_test = async_mock_service(hass, "esphome", "test") + mock_esphome_test = async_mock_service(hass, DOMAIN, "test") device.mock_service_call( HomeassistantServiceCall( service="esphome.test", @@ -1073,7 +1073,7 @@ async def async_disconnect(*args, **kwargs) -> None: new_combined_info = AsyncMock(return_value=(device_info, [], [])) mock_client.device_info_and_list_entities = new_combined_info result = await hass.config_entries.flow.async_init( - "esphome", context={"source": config_entries.SOURCE_DHCP}, data=service_info + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=service_info ) assert result["type"] is FlowResultType.ABORT @@ -1153,7 +1153,7 @@ async def async_disconnect(*args, **kwargs) -> None: new_combined_info = AsyncMock(return_value=(device_info, [], [])) mock_client.device_info_and_list_entities = new_combined_info result = await hass.config_entries.flow.async_init( - "esphome", context={"source": config_entries.SOURCE_DHCP}, data=service_info + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=service_info ) assert result["type"] is FlowResultType.ABORT diff --git a/tests/components/fan/test_device_action.py b/tests/components/fan/test_device_action.py index 747c75ed7545c2..3f1e58a2c01704 100644 --- a/tests/components/fan/test_device_action.py +++ b/tests/components/fan/test_device_action.py @@ -160,9 +160,9 @@ async def test_action( }, ) - turn_off_calls = async_mock_service(hass, "fan", "turn_off") - turn_on_calls = async_mock_service(hass, "fan", "turn_on") - toggle_calls = async_mock_service(hass, "fan", "toggle") + turn_off_calls = async_mock_service(hass, DOMAIN, "turn_off") + turn_on_calls = async_mock_service(hass, DOMAIN, "turn_on") + toggle_calls = async_mock_service(hass, DOMAIN, "toggle") hass.bus.async_fire("test_event_turn_off") await hass.async_block_till_done() @@ -223,7 +223,7 @@ async def test_action_legacy( }, ) - turn_off_calls = async_mock_service(hass, "fan", "turn_off") + turn_off_calls = async_mock_service(hass, DOMAIN, "turn_off") hass.bus.async_fire("test_event_turn_off") await hass.async_block_till_done() diff --git a/tests/components/fan/test_reproduce_state.py b/tests/components/fan/test_reproduce_state.py index 79f427495ae144..fb5fa2b7fd653d 100644 --- a/tests/components/fan/test_reproduce_state.py +++ b/tests/components/fan/test_reproduce_state.py @@ -10,6 +10,7 @@ ATTR_PRESET_MODES, DIRECTION_FORWARD, DIRECTION_REVERSE, + DOMAIN, ) from homeassistant.core import HomeAssistant, State from homeassistant.helpers.state import async_reproduce_state @@ -27,11 +28,11 @@ async def test_reproducing_states( hass.states.async_set("fan.entity_oscillating", "on", {"oscillating": True}) hass.states.async_set("fan.entity_direction", "on", {"direction": "forward"}) - turn_on_calls = async_mock_service(hass, "fan", "turn_on") - turn_off_calls = async_mock_service(hass, "fan", "turn_off") - set_direction_calls = async_mock_service(hass, "fan", "set_direction") - oscillate_calls = async_mock_service(hass, "fan", "oscillate") - set_percentage_calls = async_mock_service(hass, "fan", "set_percentage") + turn_on_calls = async_mock_service(hass, DOMAIN, "turn_on") + turn_off_calls = async_mock_service(hass, DOMAIN, "turn_off") + set_direction_calls = async_mock_service(hass, DOMAIN, "set_direction") + oscillate_calls = async_mock_service(hass, DOMAIN, "oscillate") + set_percentage_calls = async_mock_service(hass, DOMAIN, "set_percentage") # These calls should do nothing as entities already in desired state await async_reproduce_state( @@ -177,12 +178,12 @@ async def test_modern_turn_on_invalid(hass: HomeAssistant, start_state) -> None: """Test modern fan state reproduction, turning on with invalid state.""" hass.states.async_set(MODERN_FAN_ENTITY, "off", start_state) - turn_on_calls = async_mock_service(hass, "fan", "turn_on") - turn_off_calls = async_mock_service(hass, "fan", "turn_off") - set_direction_calls = async_mock_service(hass, "fan", "set_direction") - oscillate_calls = async_mock_service(hass, "fan", "oscillate") - set_percentage_mode = async_mock_service(hass, "fan", "set_percentage") - set_preset_mode = async_mock_service(hass, "fan", "set_preset_mode") + turn_on_calls = async_mock_service(hass, DOMAIN, "turn_on") + turn_off_calls = async_mock_service(hass, DOMAIN, "turn_off") + set_direction_calls = async_mock_service(hass, DOMAIN, "set_direction") + oscillate_calls = async_mock_service(hass, DOMAIN, "oscillate") + set_percentage_mode = async_mock_service(hass, DOMAIN, "set_percentage") + set_preset_mode = async_mock_service(hass, DOMAIN, "set_preset_mode") # Turn on with an invalid config (speed, percentage, preset_modes all None) await async_reproduce_state( @@ -227,12 +228,12 @@ async def test_modern_turn_on_percentage_from_different_speed( """ hass.states.async_set(MODERN_FAN_ENTITY, "off", start_state) - turn_on_calls = async_mock_service(hass, "fan", "turn_on") - turn_off_calls = async_mock_service(hass, "fan", "turn_off") - set_direction_calls = async_mock_service(hass, "fan", "set_direction") - oscillate_calls = async_mock_service(hass, "fan", "oscillate") - set_percentage_mode = async_mock_service(hass, "fan", "set_percentage") - set_preset_mode = async_mock_service(hass, "fan", "set_preset_mode") + turn_on_calls = async_mock_service(hass, DOMAIN, "turn_on") + turn_off_calls = async_mock_service(hass, DOMAIN, "turn_off") + set_direction_calls = async_mock_service(hass, DOMAIN, "set_direction") + oscillate_calls = async_mock_service(hass, DOMAIN, "oscillate") + set_percentage_mode = async_mock_service(hass, DOMAIN, "set_percentage") + set_preset_mode = async_mock_service(hass, DOMAIN, "set_preset_mode") await async_reproduce_state( hass, [State(MODERN_FAN_ENTITY, "on", MODERN_FAN_ON_PERCENTAGE15_STATE)] @@ -259,12 +260,12 @@ async def test_modern_turn_on_percentage_from_same_speed(hass: HomeAssistant) -> """ hass.states.async_set(MODERN_FAN_ENTITY, "off", MODERN_FAN_OFF_PERCENTAGE15_STATE) - turn_on_calls = async_mock_service(hass, "fan", "turn_on") - turn_off_calls = async_mock_service(hass, "fan", "turn_off") - set_direction_calls = async_mock_service(hass, "fan", "set_direction") - oscillate_calls = async_mock_service(hass, "fan", "oscillate") - set_percentage_mode = async_mock_service(hass, "fan", "set_percentage") - set_preset_mode = async_mock_service(hass, "fan", "set_preset_mode") + turn_on_calls = async_mock_service(hass, DOMAIN, "turn_on") + turn_off_calls = async_mock_service(hass, DOMAIN, "turn_off") + set_direction_calls = async_mock_service(hass, DOMAIN, "set_direction") + oscillate_calls = async_mock_service(hass, DOMAIN, "oscillate") + set_percentage_mode = async_mock_service(hass, DOMAIN, "set_percentage") + set_preset_mode = async_mock_service(hass, DOMAIN, "set_preset_mode") await async_reproduce_state( hass, [State(MODERN_FAN_ENTITY, "on", MODERN_FAN_ON_PERCENTAGE15_STATE)] @@ -301,12 +302,12 @@ async def test_modern_turn_on_preset_mode_from_different_speed( """ hass.states.async_set(MODERN_FAN_ENTITY, "off", start_state) - turn_on_calls = async_mock_service(hass, "fan", "turn_on") - turn_off_calls = async_mock_service(hass, "fan", "turn_off") - set_direction_calls = async_mock_service(hass, "fan", "set_direction") - oscillate_calls = async_mock_service(hass, "fan", "oscillate") - set_percentage_mode = async_mock_service(hass, "fan", "set_percentage") - set_preset_mode = async_mock_service(hass, "fan", "set_preset_mode") + turn_on_calls = async_mock_service(hass, DOMAIN, "turn_on") + turn_off_calls = async_mock_service(hass, DOMAIN, "turn_off") + set_direction_calls = async_mock_service(hass, DOMAIN, "set_direction") + oscillate_calls = async_mock_service(hass, DOMAIN, "oscillate") + set_percentage_mode = async_mock_service(hass, DOMAIN, "set_percentage") + set_preset_mode = async_mock_service(hass, DOMAIN, "set_preset_mode") await async_reproduce_state( hass, [State(MODERN_FAN_ENTITY, "on", MODERN_FAN_ON_PRESET_MODE_AUTO_STATE)] @@ -335,12 +336,12 @@ async def test_modern_turn_on_preset_mode_from_same_speed(hass: HomeAssistant) - MODERN_FAN_ENTITY, "off", MODERN_FAN_OFF_PPRESET_MODE_AUTO_STATE ) - turn_on_calls = async_mock_service(hass, "fan", "turn_on") - turn_off_calls = async_mock_service(hass, "fan", "turn_off") - set_direction_calls = async_mock_service(hass, "fan", "set_direction") - oscillate_calls = async_mock_service(hass, "fan", "oscillate") - set_percentage_mode = async_mock_service(hass, "fan", "set_percentage") - set_preset_mode = async_mock_service(hass, "fan", "set_preset_mode") + turn_on_calls = async_mock_service(hass, DOMAIN, "turn_on") + turn_off_calls = async_mock_service(hass, DOMAIN, "turn_off") + set_direction_calls = async_mock_service(hass, DOMAIN, "set_direction") + oscillate_calls = async_mock_service(hass, DOMAIN, "oscillate") + set_percentage_mode = async_mock_service(hass, DOMAIN, "set_percentage") + set_preset_mode = async_mock_service(hass, DOMAIN, "set_preset_mode") await async_reproduce_state( hass, [State(MODERN_FAN_ENTITY, "on", MODERN_FAN_ON_PRESET_MODE_AUTO_STATE)] @@ -377,12 +378,12 @@ async def test_modern_turn_on_preset_mode_reverse( """ hass.states.async_set(MODERN_FAN_ENTITY, "off", start_state) - turn_on_calls = async_mock_service(hass, "fan", "turn_on") - turn_off_calls = async_mock_service(hass, "fan", "turn_off") - set_direction_calls = async_mock_service(hass, "fan", "set_direction") - oscillate_calls = async_mock_service(hass, "fan", "oscillate") - set_percentage_mode = async_mock_service(hass, "fan", "set_percentage") - set_preset_mode = async_mock_service(hass, "fan", "set_preset_mode") + turn_on_calls = async_mock_service(hass, DOMAIN, "turn_on") + turn_off_calls = async_mock_service(hass, DOMAIN, "turn_off") + set_direction_calls = async_mock_service(hass, DOMAIN, "set_direction") + oscillate_calls = async_mock_service(hass, DOMAIN, "oscillate") + set_percentage_mode = async_mock_service(hass, DOMAIN, "set_percentage") + set_preset_mode = async_mock_service(hass, DOMAIN, "set_preset_mode") await async_reproduce_state( hass, @@ -420,12 +421,12 @@ async def test_modern_to_preset(hass: HomeAssistant, start_state) -> None: """Test modern fan state reproduction, switching to preset mode "Auto".""" hass.states.async_set(MODERN_FAN_ENTITY, "on", start_state) - turn_on_calls = async_mock_service(hass, "fan", "turn_on") - turn_off_calls = async_mock_service(hass, "fan", "turn_off") - set_direction_calls = async_mock_service(hass, "fan", "set_direction") - oscillate_calls = async_mock_service(hass, "fan", "oscillate") - set_percentage_mode = async_mock_service(hass, "fan", "set_percentage") - set_preset_mode = async_mock_service(hass, "fan", "set_preset_mode") + turn_on_calls = async_mock_service(hass, DOMAIN, "turn_on") + turn_off_calls = async_mock_service(hass, DOMAIN, "turn_off") + set_direction_calls = async_mock_service(hass, DOMAIN, "set_direction") + oscillate_calls = async_mock_service(hass, DOMAIN, "oscillate") + set_percentage_mode = async_mock_service(hass, DOMAIN, "set_percentage") + set_preset_mode = async_mock_service(hass, DOMAIN, "set_preset_mode") await async_reproduce_state( hass, [State(MODERN_FAN_ENTITY, "on", MODERN_FAN_ON_PRESET_MODE_AUTO_STATE)] @@ -456,12 +457,12 @@ async def test_modern_to_percentage(hass: HomeAssistant, start_state) -> None: """Test modern fan state reproduction, switching to 15% speed.""" hass.states.async_set(MODERN_FAN_ENTITY, "on", start_state) - turn_on_calls = async_mock_service(hass, "fan", "turn_on") - turn_off_calls = async_mock_service(hass, "fan", "turn_off") - set_direction_calls = async_mock_service(hass, "fan", "set_direction") - oscillate_calls = async_mock_service(hass, "fan", "oscillate") - set_percentage_mode = async_mock_service(hass, "fan", "set_percentage") - set_preset_mode = async_mock_service(hass, "fan", "set_preset_mode") + turn_on_calls = async_mock_service(hass, DOMAIN, "turn_on") + turn_off_calls = async_mock_service(hass, DOMAIN, "turn_off") + set_direction_calls = async_mock_service(hass, DOMAIN, "set_direction") + oscillate_calls = async_mock_service(hass, DOMAIN, "oscillate") + set_percentage_mode = async_mock_service(hass, DOMAIN, "set_percentage") + set_preset_mode = async_mock_service(hass, DOMAIN, "set_preset_mode") await async_reproduce_state( hass, [State(MODERN_FAN_ENTITY, "on", MODERN_FAN_ON_PERCENTAGE15_STATE)] @@ -484,12 +485,12 @@ async def test_modern_direction(hass: HomeAssistant) -> None: """Test modern fan state reproduction, switching only direction state.""" hass.states.async_set(MODERN_FAN_ENTITY, "on", MODERN_FAN_ON_PRESET_MODE_AUTO_STATE) - turn_on_calls = async_mock_service(hass, "fan", "turn_on") - turn_off_calls = async_mock_service(hass, "fan", "turn_off") - set_direction_calls = async_mock_service(hass, "fan", "set_direction") - oscillate_calls = async_mock_service(hass, "fan", "oscillate") - set_percentage_mode = async_mock_service(hass, "fan", "set_percentage") - set_preset_mode = async_mock_service(hass, "fan", "set_preset_mode") + turn_on_calls = async_mock_service(hass, DOMAIN, "turn_on") + turn_off_calls = async_mock_service(hass, DOMAIN, "turn_off") + set_direction_calls = async_mock_service(hass, DOMAIN, "set_direction") + oscillate_calls = async_mock_service(hass, DOMAIN, "oscillate") + set_percentage_mode = async_mock_service(hass, DOMAIN, "set_percentage") + set_preset_mode = async_mock_service(hass, DOMAIN, "set_preset_mode") await async_reproduce_state( hass, diff --git a/tests/components/foscam/test_config_flow.py b/tests/components/foscam/test_config_flow.py index 235a2741992a4e..d8a6db731d8316 100644 --- a/tests/components/foscam/test_config_flow.py +++ b/tests/components/foscam/test_config_flow.py @@ -4,6 +4,7 @@ from homeassistant import config_entries from homeassistant.components.foscam import config_flow +from homeassistant.components.foscam.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -17,7 +18,7 @@ async def test_user_valid(hass: HomeAssistant) -> None: """Test valid config from user input.""" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {} @@ -51,7 +52,7 @@ async def test_user_invalid_auth(hass: HomeAssistant) -> None: """Test we handle invalid auth from user input.""" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {} @@ -79,7 +80,7 @@ async def test_user_cannot_connect(hass: HomeAssistant) -> None: """Test we handle cannot connect error from user input.""" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {} @@ -107,7 +108,7 @@ async def test_user_invalid_response(hass: HomeAssistant) -> None: """Test we handle invalid response error from user input.""" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {} @@ -137,13 +138,13 @@ async def test_user_already_configured(hass: HomeAssistant) -> None: """Test we handle already configured from user input.""" entry = MockConfigEntry( - domain=config_flow.DOMAIN, + domain=DOMAIN, data=VALID_CONFIG, ) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {} @@ -168,7 +169,7 @@ async def test_user_unknown_exception(hass: HomeAssistant) -> None: """Test we handle unknown exceptions from user input.""" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {} diff --git a/tests/components/gogogate2/test_config_flow.py b/tests/components/gogogate2/test_config_flow.py index 791b93185d273b..6df0a7b2f5a685 100644 --- a/tests/components/gogogate2/test_config_flow.py +++ b/tests/components/gogogate2/test_config_flow.py @@ -50,7 +50,7 @@ async def test_auth_fail( GogoGate2ApiErrorCode.CREDENTIALS_INCORRECT, "blah" ) result = await hass.config_entries.flow.async_init( - "gogogate2", context={"source": SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -70,7 +70,7 @@ async def test_auth_fail( api.reset_mock() api.async_info.side_effect = Exception("Generic connection error.") result = await hass.config_entries.flow.async_init( - "gogogate2", context={"source": SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -88,7 +88,7 @@ async def test_auth_fail( api.reset_mock() api.async_info.side_effect = ApiError(0, "blah") result = await hass.config_entries.flow.async_init( - "gogogate2", context={"source": SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( result["flow_id"], diff --git a/tests/components/google_mail/test_config_flow.py b/tests/components/google_mail/test_config_flow.py index 8b8aaa57871cf3..654fc5c578f6fa 100644 --- a/tests/components/google_mail/test_config_flow.py +++ b/tests/components/google_mail/test_config_flow.py @@ -24,7 +24,7 @@ async def test_full_flow( ) -> None: """Check full flow.""" result = await hass.config_entries.flow.async_init( - "google_mail", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) state = config_entry_oauth2_flow._encode_jwt( hass, @@ -188,7 +188,7 @@ async def test_already_configured( config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - "google_mail", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) state = config_entry_oauth2_flow._encode_jwt( hass, diff --git a/tests/components/google_sheets/test_config_flow.py b/tests/components/google_sheets/test_config_flow.py index 5904dc283f1249..961efa0d393345 100644 --- a/tests/components/google_sheets/test_config_flow.py +++ b/tests/components/google_sheets/test_config_flow.py @@ -60,7 +60,7 @@ async def test_full_flow( ) -> None: """Check full flow.""" result = await hass.config_entries.flow.async_init( - "google_sheets", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) state = config_entry_oauth2_flow._encode_jwt( hass, @@ -127,7 +127,7 @@ async def test_create_sheet_error( ) -> None: """Test case where creating the spreadsheet fails.""" result = await hass.config_entries.flow.async_init( - "google_sheets", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) state = config_entry_oauth2_flow._encode_jwt( hass, @@ -341,7 +341,7 @@ async def test_already_configured( config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - "google_sheets", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) state = config_entry_oauth2_flow._encode_jwt( hass, diff --git a/tests/components/google_tasks/test_config_flow.py b/tests/components/google_tasks/test_config_flow.py index 29c22b343611fa..bb11817371af91 100644 --- a/tests/components/google_tasks/test_config_flow.py +++ b/tests/components/google_tasks/test_config_flow.py @@ -53,7 +53,7 @@ async def test_full_flow( ) -> None: """Check full flow.""" result = await hass.config_entries.flow.async_init( - "google_tasks", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) state = config_entry_oauth2_flow._encode_jwt( hass, @@ -108,7 +108,7 @@ async def test_api_not_enabled( ) -> None: """Check flow aborts if api is not enabled.""" result = await hass.config_entries.flow.async_init( - "google_tasks", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) state = config_entry_oauth2_flow._encode_jwt( hass, @@ -176,7 +176,7 @@ async def test_general_exception( ) -> None: """Check flow aborts if exception happens.""" result = await hass.config_entries.flow.async_init( - "google_tasks", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) state = config_entry_oauth2_flow._encode_jwt( hass, diff --git a/tests/components/group/test_fan.py b/tests/components/group/test_fan.py index b89bd9a3915dde..8ebc04ba6591b6 100644 --- a/tests/components/group/test_fan.py +++ b/tests/components/group/test_fan.py @@ -22,7 +22,7 @@ SERVICE_TURN_ON, FanEntityFeature, ) -from homeassistant.components.group import SERVICE_RELOAD +from homeassistant.components.group import DOMAIN, SERVICE_RELOAD from homeassistant.components.group.fan import DEFAULT_NAME from homeassistant.const import ( ATTR_ASSUMED_STATE, @@ -415,7 +415,7 @@ async def test_reload(hass: HomeAssistant) -> None: yaml_path = get_fixture_path("fan_configuration.yaml", "group") with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( - "group", + DOMAIN, SERVICE_RELOAD, {}, blocking=True, diff --git a/tests/components/group/test_notify.py b/tests/components/group/test_notify.py index aece5976eef1cf..58016f8c859df3 100644 --- a/tests/components/group/test_notify.py +++ b/tests/components/group/test_notify.py @@ -270,7 +270,7 @@ async def test_reload_notify(hass: HomeAssistant, tmp_path: Path) -> None: with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( - "group", + DOMAIN, SERVICE_RELOAD, {}, blocking=True, diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index e6f2dd34a4bb40..93bbdc0f029602 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -541,19 +541,19 @@ async def test_service_calls( supervisor_client.reset_mock() await hass.services.async_call( - "hassio", f"{app_or_addon}_start", {app_or_addon: "test"} + DOMAIN, f"{app_or_addon}_start", {app_or_addon: "test"} ) await hass.services.async_call( - "hassio", f"{app_or_addon}_stop", {app_or_addon: "test"} + DOMAIN, f"{app_or_addon}_stop", {app_or_addon: "test"} ) await hass.services.async_call( - "hassio", f"{app_or_addon}_restart", {app_or_addon: "test"} + DOMAIN, f"{app_or_addon}_restart", {app_or_addon: "test"} ) await hass.services.async_call( - "hassio", f"{app_or_addon}_stdin", {app_or_addon: "test", "input": "test"} + DOMAIN, f"{app_or_addon}_stdin", {app_or_addon: "test", "input": "test"} ) await hass.services.async_call( - "hassio", + DOMAIN, f"{app_or_addon}_stdin", {app_or_addon: "test", "input": {"hello": "world"}}, ) @@ -570,8 +570,8 @@ async def test_service_calls( in supervisor_client.addons.write_addon_stdin.mock_calls ) - await hass.services.async_call("hassio", "host_shutdown", {}) - await hass.services.async_call("hassio", "host_reboot", {}) + await hass.services.async_call(DOMAIN, "host_shutdown", {}) + await hass.services.async_call(DOMAIN, "host_reboot", {}) await hass.async_block_till_done() supervisor_client.host.shutdown.assert_called_once_with() @@ -585,7 +585,7 @@ async def test_service_calls( ) full_backup = await hass.services.async_call( - "hassio", "backup_full", {}, blocking=True, return_response=True + DOMAIN, "backup_full", {}, blocking=True, return_response=True ) supervisor_client.backups.full_backup.assert_called_once_with( FullBackupOptions(name="2021-11-13 03:48:00") @@ -593,7 +593,7 @@ async def test_service_calls( assert full_backup == {"backup": "full"} partial_backup = await hass.services.async_call( - "hassio", + DOMAIN, "backup_partial", { "homeassistant": True, @@ -615,9 +615,9 @@ async def test_service_calls( ) assert partial_backup == {"backup": "partial"} - await hass.services.async_call("hassio", "restore_full", {"slug": "test"}) + await hass.services.async_call(DOMAIN, "restore_full", {"slug": "test"}) await hass.services.async_call( - "hassio", + DOMAIN, "restore_partial", { "slug": "test", @@ -641,7 +641,7 @@ async def test_service_calls( ) await hass.services.async_call( - "hassio", + DOMAIN, "backup_full", { "name": "backup_name", @@ -659,7 +659,7 @@ async def test_service_calls( ) await hass.services.async_call( - "hassio", + DOMAIN, "backup_full", { "location": "/backup", @@ -675,7 +675,7 @@ async def test_service_calls( await hass.async_block_till_done() await hass.services.async_call( - "hassio", + DOMAIN, "backup_full", { "location": "/backup", @@ -699,11 +699,11 @@ async def test_invalid_service_calls(hass: HomeAssistant, app_or_addon: str) -> with pytest.raises(Invalid): await hass.services.async_call( - "hassio", f"{app_or_addon}_start", {app_or_addon: "inv@lid"} + DOMAIN, f"{app_or_addon}_start", {app_or_addon: "inv@lid"} ) with pytest.raises(Invalid): await hass.services.async_call( - "hassio", + DOMAIN, f"{app_or_addon}_stdin", {app_or_addon: "inv@lid", "input": "test"}, ) @@ -738,7 +738,7 @@ async def test_service_calls_apps_addons_exclusive( with pytest.raises( Invalid, match="two or more values in the same group of exclusion" ): - await hass.services.async_call("hassio", service, service_data) + await hass.services.async_call(DOMAIN, service, service_data) @pytest.mark.parametrize( @@ -779,7 +779,7 @@ async def test_addon_service_call_with_complex_slug( await hass.async_block_till_done() await hass.services.async_call( - "hassio", f"{app_or_addon}_start", {app_or_addon: "test.a_1-2"} + DOMAIN, f"{app_or_addon}_start", {app_or_addon: "test.a_1-2"} ) @@ -823,12 +823,12 @@ async def test_invalid_service_calls_app_duplicates( with pytest.raises(Invalid, match="contains duplicate items"): await hass.services.async_call( - "hassio", "backup_partial", {app_or_addon: ["test", "test"]} + DOMAIN, "backup_partial", {app_or_addon: ["test", "test"]} ) with pytest.raises(Invalid, match="contains duplicate items"): await hass.services.async_call( - "hassio", "restore_partial", {app_or_addon: ["test", "test"]} + DOMAIN, "restore_partial", {app_or_addon: ["test", "test"]} ) @@ -839,12 +839,12 @@ async def test_invalid_service_calls_folder_duplicates(hass: HomeAssistant) -> N with pytest.raises(Invalid, match="contains duplicate items"): await hass.services.async_call( - "hassio", "backup_partial", {"folders": ["ssl", "ssl"]} + DOMAIN, "backup_partial", {"folders": ["ssl", "ssl"]} ) with pytest.raises(Invalid, match="contains duplicate items"): await hass.services.async_call( - "hassio", "restore_partial", {"folders": ["ssl", "ssl"]} + DOMAIN, "restore_partial", {"folders": ["ssl", "ssl"]} ) @@ -859,7 +859,7 @@ async def test_partial_backup_legacy_homeassistant_folder( ) await hass.services.async_call( - "hassio", + DOMAIN, "backup_partial", {"folders": ["homeassistant", "ssl"], "name": "test"}, blocking=True, @@ -886,7 +886,7 @@ async def test_partial_restore_legacy_homeassistant_folder( assert await async_setup_component(hass, "hassio", {}) await hass.services.async_call( - "hassio", + DOMAIN, "restore_partial", {"slug": "test", "folders": ["homeassistant", "ssl"]}, blocking=True, @@ -906,9 +906,7 @@ async def test_partial_backup_invalid_folder(hass: HomeAssistant) -> None: assert await async_setup_component(hass, "hassio", {}) with pytest.raises(Invalid, match="not a valid value"): - await hass.services.async_call( - "hassio", "backup_partial", {"folders": ["bogus"]} - ) + await hass.services.async_call(DOMAIN, "backup_partial", {"folders": ["bogus"]}) @pytest.mark.usefixtures("hassio_env", "supervisor_client") @@ -920,7 +918,7 @@ async def test_partial_backup_legacy_homeassistant_folder_conflict( with pytest.raises(ServiceValidationError, match="conflicts"): await hass.services.async_call( - "hassio", + DOMAIN, "backup_partial", {"homeassistant": False, "folders": ["homeassistant"]}, blocking=True, @@ -1569,7 +1567,7 @@ async def test_mount_reload_action( """Test reload_mount service call.""" device = await mount_reload_test_setup(hass, device_registry, supervisor_client) await hass.services.async_call( - "hassio", "mount_reload", {"device_id": device.id}, blocking=True + DOMAIN, "mount_reload", {"device_id": device.id}, blocking=True ) supervisor_client.mounts.reload_mount.assert_awaited_once_with("NAS") @@ -1586,7 +1584,7 @@ async def test_mount_reload_action_failure( ) with pytest.raises(HomeAssistantError) as exc: await hass.services.async_call( - "hassio", "mount_reload", {"device_id": device.id}, blocking=True + DOMAIN, "mount_reload", {"device_id": device.id}, blocking=True ) assert str(exc.value) == "Failed to reload mount NAS: test failure" @@ -1600,7 +1598,7 @@ async def test_mount_reload_unknown_device_id( await mount_reload_test_setup(hass, device_registry, supervisor_client) with pytest.raises(ServiceValidationError) as exc: await hass.services.async_call( - "hassio", "mount_reload", {"device_id": "1234"}, blocking=True + DOMAIN, "mount_reload", {"device_id": "1234"}, blocking=True ) assert str(exc.value) == "Device ID not found" @@ -1615,7 +1613,7 @@ async def test_mount_reload_no_name( device_registry.async_update_device(device.id, name=None) with pytest.raises(ServiceValidationError) as exc: await hass.services.async_call( - "hassio", "mount_reload", {"device_id": device.id}, blocking=True + DOMAIN, "mount_reload", {"device_id": device.id}, blocking=True ) assert str(exc.value) == "Device is not a supervisor mount point" @@ -1630,7 +1628,7 @@ async def test_mount_reload_invalid_model( device_registry.async_update_device(device.id, model=None) with pytest.raises(ServiceValidationError) as exc: await hass.services.async_call( - "hassio", "mount_reload", {"device_id": device.id}, blocking=True + DOMAIN, "mount_reload", {"device_id": device.id}, blocking=True ) assert str(exc.value) == "Device is not a supervisor mount point" @@ -1652,7 +1650,7 @@ async def test_mount_reload_not_supervisor_device( ) with pytest.raises(ServiceValidationError) as exc: await hass.services.async_call( - "hassio", "mount_reload", {"device_id": device2.id}, blocking=True + DOMAIN, "mount_reload", {"device_id": device2.id}, blocking=True ) assert str(exc.value) == "Device is not a supervisor mount point" diff --git a/tests/components/helty/snapshots/test_button.ambr b/tests/components/helty/snapshots/test_button.ambr new file mode 100644 index 00000000000000..51abecbf67bd79 --- /dev/null +++ b/tests/components/helty/snapshots/test_button.ambr @@ -0,0 +1,51 @@ +# serializer version: 1 +# name: test_all_entities[button.vmc_soggiorno_reset_filter-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'button', + 'entity_category': , + 'entity_id': 'button.vmc_soggiorno_reset_filter', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Reset filter', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Reset filter', + 'platform': 'helty', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'reset_filter', + 'unique_id': '01HHHHHHHHHHHHHHHHHHHHHHHH_reset_filter', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[button.vmc_soggiorno_reset_filter-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'VMC Soggiorno Reset filter', + }), + 'context': , + 'entity_id': 'button.vmc_soggiorno_reset_filter', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/helty/test_button.py b/tests/components/helty/test_button.py new file mode 100644 index 00000000000000..9f7d8a81c94d0e --- /dev/null +++ b/tests/components/helty/test_button.py @@ -0,0 +1,69 @@ +"""Test the Helty Flow button platform.""" + +from unittest.mock import AsyncMock, patch + +from pyhelty import HeltyError +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + +RESET_FILTER_ENTITY = "button.vmc_soggiorno_reset_filter" + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_helty_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.helty.PLATFORMS", [Platform.BUTTON]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_reset_filter_press( + hass: HomeAssistant, + mock_helty_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test pressing the reset filter button calls the client.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: RESET_FILTER_ENTITY}, + blocking=True, + ) + mock_helty_client.async_reset_filter.assert_awaited_once() + + +async def test_reset_filter_press_error( + hass: HomeAssistant, + mock_helty_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test a device failure during reset raises a HomeAssistantError.""" + await setup_integration(hass, mock_config_entry) + + mock_helty_client.async_reset_filter.side_effect = HeltyError + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + BUTTON_DOMAIN, + SERVICE_PRESS, + {ATTR_ENTITY_ID: RESET_FILTER_ENTITY}, + blocking=True, + ) diff --git a/tests/components/helty/test_fan.py b/tests/components/helty/test_fan.py index 0cb3350d842404..807bf634d596ab 100644 --- a/tests/components/helty/test_fan.py +++ b/tests/components/helty/test_fan.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, patch -from pyhelty import FanMode +from pyhelty import FanMode, HeltyError import pytest from syrupy.assertion import SnapshotAssertion @@ -20,6 +20,7 @@ Platform, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from . import setup_integration @@ -166,3 +167,22 @@ async def test_fan_turn_on_with_preset( blocking=True, ) mock_helty_client.async_set_fan_mode.assert_awaited_with(FanMode.FREE_COOLING) + + +async def test_fan_set_mode_error( + hass: HomeAssistant, + mock_helty_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test a device failure while setting the mode raises a HomeAssistantError.""" + await setup_integration(hass, mock_config_entry) + + mock_helty_client.async_set_fan_mode.side_effect = HeltyError + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_PRESET_MODE, + {ATTR_ENTITY_ID: FAN_ENTITY, ATTR_PRESET_MODE: "boost"}, + blocking=True, + ) diff --git a/tests/components/home_connect/test_config_flow.py b/tests/components/home_connect/test_config_flow.py index 3355faa7af0f80..b57d2855db12b4 100644 --- a/tests/components/home_connect/test_config_flow.py +++ b/tests/components/home_connect/test_config_flow.py @@ -104,7 +104,7 @@ async def test_full_flow( assert await setup.async_setup_component(hass, "home_connect", {}) result = await hass.config_entries.flow.async_init( - "home_connect", context=ConfigFlowContext(source=config_entries.SOURCE_USER) + DOMAIN, context=ConfigFlowContext(source=config_entries.SOURCE_USER) ) state = config_entry_oauth2_flow._encode_jwt( hass, @@ -159,7 +159,7 @@ async def test_prevent_reconfiguring_same_account( assert await setup.async_setup_component(hass, "home_connect", {}) result = await hass.config_entries.flow.async_init( - "home_connect", context=ConfigFlowContext(source=config_entries.SOURCE_USER) + DOMAIN, context=ConfigFlowContext(source=config_entries.SOURCE_USER) ) state = config_entry_oauth2_flow._encode_jwt( hass, diff --git a/tests/components/homeassistant/test_init.py b/tests/components/homeassistant/test_init.py index dd5f9bd1aaa7ef..a55ca38d5a2335 100644 --- a/tests/components/homeassistant/test_init.py +++ b/tests/components/homeassistant/test_init.py @@ -248,7 +248,7 @@ async def test_entity_update(hass: HomeAssistant) -> None: return_value=None, ) as mock_update: await hass.services.async_call( - "homeassistant", + DOMAIN, "update_entity", {"entity_id": ["light.kitchen"]}, blocking=True, @@ -268,7 +268,7 @@ async def test_setting_location(hass: HomeAssistant) -> None: elevation = hass.config.elevation assert elevation != 50 await hass.services.async_call( - "homeassistant", + DOMAIN, SERVICE_SET_LOCATION, {"latitude": 30, "longitude": 40}, blocking=True, @@ -279,7 +279,7 @@ async def test_setting_location(hass: HomeAssistant) -> None: assert hass.config.elevation == elevation await hass.services.async_call( - "homeassistant", + DOMAIN, SERVICE_SET_LOCATION, {"latitude": 30, "longitude": 40, "elevation": 50}, blocking=True, @@ -289,7 +289,7 @@ async def test_setting_location(hass: HomeAssistant) -> None: assert hass.config.elevation == 50 await hass.services.async_call( - "homeassistant", + DOMAIN, SERVICE_SET_LOCATION, {"latitude": 30, "longitude": 40, "elevation": 0}, blocking=True, @@ -387,7 +387,7 @@ async def test_reload_config_entry_by_entity_id( return_value=None, ) as mock_reload: await hass.services.async_call( - "homeassistant", + DOMAIN, "reload_config_entry", {"entity_id": f"{reg_entity1.entity_id},{reg_entity2.entity_id}"}, blocking=True, @@ -401,7 +401,7 @@ async def test_reload_config_entry_by_entity_id( with pytest.raises(ValueError): await hass.services.async_call( - "homeassistant", + DOMAIN, "reload_config_entry", {"entity_id": "unknown.entity_id"}, blocking=True, @@ -417,7 +417,7 @@ async def test_reload_config_entry_by_entry_id(hass: HomeAssistant) -> None: return_value=None, ) as mock_reload: await hass.services.async_call( - "homeassistant", + DOMAIN, "reload_config_entry", {ATTR_ENTRY_ID: "8955375327824e14ba89e4b29cc3ec9a"}, blocking=True, @@ -444,7 +444,7 @@ async def test_raises_when_db_upgrade_in_progress( ) as mock_async_migration_in_progress, ): await hass.services.async_call( - "homeassistant", + DOMAIN, service, blocking=True, ) @@ -462,7 +462,7 @@ async def test_raises_when_db_upgrade_in_progress( patch("homeassistant.config.async_check_ha_config_file", return_value=None), ): await hass.services.async_call( - "homeassistant", + DOMAIN, service, blocking=True, ) @@ -489,7 +489,7 @@ async def test_raises_when_config_is_invalid( ) as mock_async_check_ha_config_file, ): await hass.services.async_call( - "homeassistant", + DOMAIN, SERVICE_HOMEASSISTANT_RESTART, blocking=True, ) @@ -510,7 +510,7 @@ async def test_raises_when_config_is_invalid( ) as mock_async_check_ha_config_file, ): await hass.services.async_call( - "homeassistant", + DOMAIN, SERVICE_HOMEASSISTANT_RESTART, blocking=True, ) @@ -537,7 +537,7 @@ async def test_restart_homeassistant( ) as mock_restart, ): await hass.services.async_call( - "homeassistant", + DOMAIN, SERVICE_HOMEASSISTANT_RESTART, service_data, blocking=True, @@ -560,7 +560,7 @@ async def test_stop_homeassistant(hass: HomeAssistant) -> None: ) as mock_restart, ): await hass.services.async_call( - "homeassistant", + DOMAIN, SERVICE_HOMEASSISTANT_STOP, blocking=True, ) @@ -577,7 +577,7 @@ async def test_save_persistent_states(hass: HomeAssistant) -> None: return_value=None, ) as mock_save: await hass.services.async_call( - "homeassistant", + DOMAIN, SERVICE_SAVE_PERSISTENT_STATES, blocking=True, ) @@ -592,7 +592,7 @@ async def test_reload_custom_templates(hass: HomeAssistant) -> None: return_value=None, ) as mock_load_custom_templates: await hass.services.async_call( - "homeassistant", + DOMAIN, SERVICE_RELOAD_CUSTOM_TEMPLATES, blocking=True, ) @@ -608,16 +608,16 @@ async def test_reload_all( test2 = async_mock_service(hass, "test2", "reload") no_reload = async_mock_service(hass, "test3", "not_reload") notify = async_mock_service(hass, "notify", "reload") - core_config = async_mock_service(hass, "homeassistant", "reload_core_config") + core_config = async_mock_service(hass, DOMAIN, "reload_core_config") themes = async_mock_service(hass, "frontend", "reload_themes") - jinja = async_mock_service(hass, "homeassistant", "reload_custom_templates") + jinja = async_mock_service(hass, DOMAIN, "reload_custom_templates") with patch( "homeassistant.config.async_check_ha_config_file", return_value=None, ) as mock_async_check_ha_config_file: await hass.services.async_call( - "homeassistant", + DOMAIN, SERVICE_RELOAD_ALL, blocking=True, ) @@ -645,7 +645,7 @@ async def test_reload_all( ) as mock_async_check_ha_config_file, ): await hass.services.async_call( - "homeassistant", + DOMAIN, SERVICE_RELOAD_ALL, blocking=True, ) diff --git a/tests/components/homee/snapshots/test_cover.ambr b/tests/components/homee/snapshots/test_cover.ambr index 8076611b1f794d..ce9806d43ef519 100644 --- a/tests/components/homee/snapshots/test_cover.ambr +++ b/tests/components/homee/snapshots/test_cover.ambr @@ -138,7 +138,7 @@ 'platform': 'homee', 'previous_unique_id': None, 'suggested_object_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': '00055511EECC-1-0', 'unit_of_measurement': None, @@ -151,7 +151,7 @@ 'device_class': 'shutter', 'friendly_name': 'Slats & Position', 'is_closed': False, - 'supported_features': , + 'supported_features': , }), 'context': , 'entity_id': 'cover.slats_position', diff --git a/tests/components/homee/test_cover.py b/tests/components/homee/test_cover.py index 38ee00f36a7a00..98e2a681de724b 100644 --- a/tests/components/homee/test_cover.py +++ b/tests/components/homee/test_cover.py @@ -29,6 +29,7 @@ SERVICE_SET_COVER_POSITION, SERVICE_SET_COVER_TILT_POSITION, SERVICE_STOP_COVER, + SERVICE_STOP_COVER_TILT, STATE_UNAVAILABLE, Platform, ) @@ -164,9 +165,16 @@ async def test_close_open_slats( assert attributes.get("supported_features") == ( CoverEntityFeature.OPEN_TILT | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.STOP_TILT | CoverEntityFeature.SET_TILT_POSITION ) + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_STOP_COVER_TILT, + {ATTR_ENTITY_ID: "cover.slats_position"}, + blocking=True, + ) await hass.services.async_call( COVER_DOMAIN, SERVICE_CLOSE_COVER_TILT, @@ -181,7 +189,7 @@ async def test_close_open_slats( ) calls = mock_homee.set_value.call_args_list - for index, call in enumerate(calls, start=1): + for index, call in enumerate(calls): assert call[0] == (mock_homee.nodes[0].id, 2, index) @@ -200,6 +208,7 @@ async def test_close_open_reversed_slats( assert attributes.get("supported_features") == ( CoverEntityFeature.OPEN_TILT | CoverEntityFeature.CLOSE_TILT + | CoverEntityFeature.STOP_TILT | CoverEntityFeature.SET_TILT_POSITION ) diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index f1029cd2698738..db977280af15f7 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -247,7 +247,7 @@ async def test_discovery_works( # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) @@ -280,7 +280,7 @@ async def test_abort_duplicate_flow(hass: HomeAssistant, controller) -> None: # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) @@ -288,7 +288,7 @@ async def test_abort_duplicate_flow(hass: HomeAssistant, controller) -> None: assert result["step_id"] == "pair" result = await hass.config_entries.flow.async_init( - "homekit_controller", + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) @@ -304,7 +304,7 @@ async def test_pair_already_paired_1(hass: HomeAssistant, controller) -> None: # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) @@ -321,7 +321,7 @@ async def test_unknown_domain_type(hass: HomeAssistant, controller) -> None: # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) @@ -339,7 +339,7 @@ async def test_id_missing(hass: HomeAssistant, controller) -> None: # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) @@ -356,7 +356,7 @@ async def test_discovery_ignored_model(hass: HomeAssistant, controller) -> None: # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) @@ -384,7 +384,7 @@ async def test_discovery_ignored_hk_bridge( # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) @@ -412,7 +412,7 @@ async def test_discovery_does_not_ignore_non_homekit( # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) @@ -443,7 +443,7 @@ async def test_discovery_broken_pairing_flag(hass: HomeAssistant, controller) -> # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) @@ -479,7 +479,7 @@ async def test_discovery_invalid_config_entry(hass: HomeAssistant, controller) - side_effect=AuthenticationError("Invalid pairing keys"), ): result = await hass.config_entries.flow.async_init( - "homekit_controller", + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) @@ -518,7 +518,7 @@ async def test_discovery_ignored_config_entry(hass: HomeAssistant, controller) - side_effect=AuthenticationError("Invalid pairing keys"), ): result = await hass.config_entries.flow.async_init( - "homekit_controller", + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) @@ -553,7 +553,7 @@ async def test_discovery_already_configured(hass: HomeAssistant, controller) -> # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) @@ -591,7 +591,7 @@ async def test_discovery_already_configured_update_csharp( # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) @@ -614,7 +614,7 @@ async def test_pair_abort_errors_on_start( # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) @@ -638,7 +638,7 @@ async def test_pair_try_later_errors_on_start( # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) @@ -678,7 +678,7 @@ async def test_pair_form_errors_on_start( # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) @@ -729,7 +729,7 @@ async def test_pair_abort_errors_on_finish( # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) @@ -772,7 +772,7 @@ async def test_pair_form_errors_on_finish( # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) @@ -819,7 +819,7 @@ async def test_pair_unknown_errors(hass: HomeAssistant, controller) -> None: # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) @@ -870,7 +870,7 @@ async def test_user_works(hass: HomeAssistant, controller) -> None: # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM @@ -907,7 +907,7 @@ async def test_user_pairing_with_insecure_setup_code( # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM @@ -946,7 +946,7 @@ async def test_user_pairing_with_insecure_setup_code( async def test_user_no_devices(hass: HomeAssistant, controller) -> None: """Test user initiated pairing where no devices discovered.""" result = await hass.config_entries.flow.async_init( - "homekit_controller", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "no_devices" @@ -962,7 +962,7 @@ async def test_user_no_unpaired_devices(hass: HomeAssistant, controller) -> None # Device discovery is requested result = await hass.config_entries.flow.async_init( - "homekit_controller", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.ABORT @@ -983,7 +983,7 @@ async def test_discovery_dismiss_existing_flow_on_paired( # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) @@ -999,7 +999,7 @@ async def test_discovery_dismiss_existing_flow_on_paired( discovery_info.properties["sf"] = 0x00 # Device is discovered again after pairing to someone else result2 = await hass.config_entries.flow.async_init( - "homekit_controller", + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) @@ -1022,7 +1022,7 @@ async def test_mdns_update_to_paired_during_pairing( # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) @@ -1072,7 +1072,7 @@ async def _finish_pairing(*args, **kwargs): # Make sure when the device is discovered as paired via mdns # it does not abort pairing if it happens before pairing is finished result2 = await hass.config_entries.flow.async_init( - "homekit_controller", + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info_paired, ) @@ -1092,7 +1092,7 @@ async def test_discovery_no_bluetooth_support(hass: HomeAssistant, controller) - False, ): result = await hass.config_entries.flow.async_init( - "homekit_controller", + DOMAIN, context={"source": config_entries.SOURCE_BLUETOOTH}, data=HK_BLUETOOTH_SERVICE_INFO_NOT_DISCOVERED, ) @@ -1107,7 +1107,7 @@ async def test_bluetooth_not_homekit(hass: HomeAssistant, controller) -> None: True, ): result = await hass.config_entries.flow.async_init( - "homekit_controller", + DOMAIN, context={"source": config_entries.SOURCE_BLUETOOTH}, data=NOT_HK_BLUETOOTH_SERVICE_INFO, ) @@ -1124,7 +1124,7 @@ async def test_bluetooth_valid_device_no_discovery( True, ): result = await hass.config_entries.flow.async_init( - "homekit_controller", + DOMAIN, context={"source": config_entries.SOURCE_BLUETOOTH}, data=HK_BLUETOOTH_SERVICE_INFO_NOT_DISCOVERED, ) @@ -1143,7 +1143,7 @@ async def test_bluetooth_valid_device_discovery_paired( True, ): result = await hass.config_entries.flow.async_init( - "homekit_controller", + DOMAIN, context={"source": config_entries.SOURCE_BLUETOOTH}, data=HK_BLUETOOTH_SERVICE_INFO_DISCOVERED_PAIRED, ) @@ -1164,7 +1164,7 @@ async def test_bluetooth_valid_device_discovery_unpaired( True, ): result = await hass.config_entries.flow.async_init( - "homekit_controller", + DOMAIN, context={"source": config_entries.SOURCE_BLUETOOTH}, data=HK_BLUETOOTH_SERVICE_INFO_DISCOVERED_UNPAIRED, ) @@ -1218,7 +1218,7 @@ async def test_discovery_updates_ip_when_config_entry_set_up( # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) @@ -1256,7 +1256,7 @@ async def test_discovery_updates_ip_config_entry_not_set_up( # Device is discovered result = await hass.config_entries.flow.async_init( - "homekit_controller", + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=discovery_info, ) diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index 9edc60d0608085..5619a99985aea1 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -387,7 +387,7 @@ async def test_creating_entry_removes_entries_for_same_host_or_bridge( assert len(hass.config_entries.async_entries("hue")) == 2 result = await hass.config_entries.flow.async_init( - "hue", + DOMAIN, data={"host": "2.2.2.2"}, context={"source": config_entries.SOURCE_IMPORT}, ) diff --git a/tests/components/hue/test_scene.py b/tests/components/hue/test_scene.py index 1ddfb6a992378f..fb0b845072d92c 100644 --- a/tests/components/hue/test_scene.py +++ b/tests/components/hue/test_scene.py @@ -4,6 +4,7 @@ import pytest +from homeassistant.components.hue import DOMAIN from homeassistant.const import STATE_UNKNOWN, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -125,7 +126,7 @@ async def test_scene_advanced_turn_on_service( # call the hue.activate_scene service await hass.services.async_call( - "hue", + DOMAIN, "activate_scene", {"entity_id": test_entity_id}, blocking=True, @@ -138,7 +139,7 @@ async def test_scene_advanced_turn_on_service( # test again with sending speed and dynamic await hass.services.async_call( - "hue", + DOMAIN, "activate_scene", {"entity_id": test_entity_id, "speed": 80, "dynamic": True}, blocking=True, diff --git a/tests/components/hue/test_services.py b/tests/components/hue/test_services.py index 2fd8379a73a56b..fd8311e503d2dc 100644 --- a/tests/components/hue/test_services.py +++ b/tests/components/hue/test_services.py @@ -3,7 +3,7 @@ from unittest.mock import Mock, patch from homeassistant.components import hue -from homeassistant.components.hue import bridge +from homeassistant.components.hue import DOMAIN, bridge from homeassistant.components.hue.const import ( CONF_ALLOW_HUE_GROUPS, CONF_ALLOW_UNREACHABLE, @@ -209,7 +209,7 @@ async def test_hue_multi_bridge_activate_scene_all_respond( hue.services, "hue_activate_scene_v2", return_value=True ) as mock_hue_activate_scene2: await hass.services.async_call( - "hue", + DOMAIN, "hue_activate_scene", {"group_name": "Group 1", "scene_name": "Cozy dinner"}, blocking=True, @@ -246,7 +246,7 @@ async def test_hue_multi_bridge_activate_scene_one_responds( hue.services, "hue_activate_scene_v2", return_value=False ) as mock_hue_activate_scene2: await hass.services.async_call( - "hue", + DOMAIN, "hue_activate_scene", {"group_name": "Group 1", "scene_name": "Cozy dinner"}, blocking=True, @@ -281,7 +281,7 @@ async def test_hue_multi_bridge_activate_scene_zero_responds( hue.services, "hue_activate_scene_v2", return_value=False ) as mock_hue_activate_scene2: await hass.services.async_call( - "hue", + DOMAIN, "hue_activate_scene", {"group_name": "Non existing group", "scene_name": "Non existing Scene"}, blocking=True, diff --git a/tests/components/humidifier/test_device_action.py b/tests/components/humidifier/test_device_action.py index 1d1f70574b00a7..2f02f03658b38e 100644 --- a/tests/components/humidifier/test_device_action.py +++ b/tests/components/humidifier/test_device_action.py @@ -228,11 +228,11 @@ async def test_action( }, ) - set_humidity_calls = async_mock_service(hass, "humidifier", "set_humidity") - set_mode_calls = async_mock_service(hass, "humidifier", "set_mode") - turn_on_calls = async_mock_service(hass, "humidifier", "turn_on") - turn_off_calls = async_mock_service(hass, "humidifier", "turn_off") - toggle_calls = async_mock_service(hass, "humidifier", "toggle") + set_humidity_calls = async_mock_service(hass, DOMAIN, "set_humidity") + set_mode_calls = async_mock_service(hass, DOMAIN, "set_mode") + turn_on_calls = async_mock_service(hass, DOMAIN, "turn_on") + turn_off_calls = async_mock_service(hass, DOMAIN, "turn_off") + toggle_calls = async_mock_service(hass, DOMAIN, "toggle") assert len(set_humidity_calls) == 0 assert len(set_mode_calls) == 0 @@ -341,7 +341,7 @@ async def test_action_legacy( }, ) - set_mode_calls = async_mock_service(hass, "humidifier", "set_mode") + set_mode_calls = async_mock_service(hass, DOMAIN, "set_mode") hass.bus.async_fire("test_event_set_mode") await hass.async_block_till_done() diff --git a/tests/components/husqvarna_automower/test_config_flow.py b/tests/components/husqvarna_automower/test_config_flow.py index dc9d5165be40b5..a4d74b2ed8be7c 100644 --- a/tests/components/husqvarna_automower/test_config_flow.py +++ b/tests/components/husqvarna_automower/test_config_flow.py @@ -47,7 +47,7 @@ async def test_full_flow( ) -> None: """Check full flow.""" result = await hass.config_entries.flow.async_init( - "husqvarna_automower", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) state = config_entry_oauth2_flow._encode_jwt( hass, diff --git a/tests/components/ifttt/test_init.py b/tests/components/ifttt/test_init.py index c6d24421a8ab60..ad4f283d5af387 100644 --- a/tests/components/ifttt/test_init.py +++ b/tests/components/ifttt/test_init.py @@ -2,6 +2,7 @@ from homeassistant import config_entries from homeassistant.components import ifttt +from homeassistant.components.ifttt import DOMAIN from homeassistant.core import HomeAssistant, callback from homeassistant.core_config import async_process_ha_core_config from homeassistant.data_entry_flow import FlowResultType @@ -19,7 +20,7 @@ async def test_config_flow_registers_webhook( ) result = await hass.config_entries.flow.async_init( - "ifttt", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM, result diff --git a/tests/components/input_boolean/test_init.py b/tests/components/input_boolean/test_init.py index b82bbe59203315..0633846f21d961 100644 --- a/tests/components/input_boolean/test_init.py +++ b/tests/components/input_boolean/test_init.py @@ -186,7 +186,7 @@ async def test_input_boolean_context( assert state is not None await hass.services.async_call( - "input_boolean", + DOMAIN, "turn_off", {"entity_id": state.entity_id}, True, diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py index f3bdcde3c01df6..474cda010695aa 100644 --- a/tests/components/input_datetime/test_init.py +++ b/tests/components/input_datetime/test_init.py @@ -286,7 +286,7 @@ async def test_set_invalid(hass: HomeAssistant) -> None: with pytest.raises(vol.Invalid): await hass.services.async_call( - "input_datetime", + DOMAIN, "set_datetime", {"entity_id": entity_id, "time": time_portion}, blocking=True, @@ -316,7 +316,7 @@ async def test_set_invalid_2(hass: HomeAssistant) -> None: with pytest.raises(vol.Invalid): await hass.services.async_call( - "input_datetime", + DOMAIN, "set_datetime", {"entity_id": entity_id, "time": time_portion, "datetime": dt_obj}, blocking=True, @@ -450,7 +450,7 @@ async def test_input_datetime_context( assert state is not None await hass.services.async_call( - "input_datetime", + DOMAIN, "set_datetime", {"entity_id": state.entity_id, "date": "2018-01-02"}, blocking=True, diff --git a/tests/components/input_datetime/test_reproduce_state.py b/tests/components/input_datetime/test_reproduce_state.py index 323849bc882478..982f3213a588b4 100644 --- a/tests/components/input_datetime/test_reproduce_state.py +++ b/tests/components/input_datetime/test_reproduce_state.py @@ -2,6 +2,7 @@ import pytest +from homeassistant.components.input_datetime import DOMAIN from homeassistant.core import HomeAssistant, State from homeassistant.helpers.state import async_reproduce_state @@ -31,7 +32,7 @@ async def test_reproducing_states( {"has_date": False, "has_time": False}, ) - datetime_calls = async_mock_service(hass, "input_datetime", "set_datetime") + datetime_calls = async_mock_service(hass, DOMAIN, "set_datetime") # These calls should do nothing as entities already in desired state await async_reproduce_state( diff --git a/tests/components/input_number/test_init.py b/tests/components/input_number/test_init.py index 94166a8ab7e75a..79dab8c2bbf0a0 100644 --- a/tests/components/input_number/test_init.py +++ b/tests/components/input_number/test_init.py @@ -334,7 +334,7 @@ async def test_input_number_context( assert state is not None await hass.services.async_call( - "input_number", + DOMAIN, "increment", {"entity_id": state.entity_id}, True, diff --git a/tests/components/insteon/test_config_flow.py b/tests/components/insteon/test_config_flow.py index 32cedc3c202c8c..de6ea29855ad54 100644 --- a/tests/components/insteon/test_config_flow.py +++ b/tests/components/insteon/test_config_flow.py @@ -280,7 +280,7 @@ async def test_discovery_via_usb(hass: HomeAssistant) -> None: manufacturer="test", ) result = await hass.config_entries.flow.async_init( - "insteon", context={"source": config_entries.SOURCE_USB}, data=discovery_info + DOMAIN, context={"source": config_entries.SOURCE_USB}, data=discovery_info ) await hass.async_block_till_done() assert result["type"] is FlowResultType.FORM @@ -312,7 +312,7 @@ async def test_discovery_via_usb_already_setup(hass: HomeAssistant) -> None: manufacturer="test", ) result = await hass.config_entries.flow.async_init( - "insteon", context={"source": config_entries.SOURCE_USB}, data=discovery_info + DOMAIN, context={"source": config_entries.SOURCE_USB}, data=discovery_info ) await hass.async_block_till_done() diff --git a/tests/components/knx/test_services.py b/tests/components/knx/test_services.py index 36a78a1f3a9e40..e4102f0d243c3a 100644 --- a/tests/components/knx/test_services.py +++ b/tests/components/knx/test_services.py @@ -115,7 +115,7 @@ async def test_send( await knx.setup_integration() await hass.services.async_call( - "knx", + DOMAIN, "send", service_payload, blocking=True, @@ -131,12 +131,12 @@ async def test_read(hass: HomeAssistant, knx: KNXTestKit) -> None: await knx.setup_integration() # send read telegram - await hass.services.async_call("knx", "read", {"address": "1/1/1"}, blocking=True) + await hass.services.async_call(DOMAIN, "read", {"address": "1/1/1"}, blocking=True) await knx.assert_read("1/1/1") # send multiple read telegrams await hass.services.async_call( - "knx", + DOMAIN, "read", {"address": ["1/1/1", "2/2/2", "3/3/3"]}, blocking=True, @@ -159,7 +159,7 @@ async def test_event_register(hass: HomeAssistant, knx: KNXTestKit) -> None: # register event with `type` await hass.services.async_call( - "knx", + DOMAIN, "event_register", {"address": test_address, "type": "2byte_unsigned"}, blocking=True, @@ -172,7 +172,7 @@ async def test_event_register(hass: HomeAssistant, knx: KNXTestKit) -> None: # remove event registration - no event added await hass.services.async_call( - "knx", + DOMAIN, "event_register", {"address": test_address, "remove": True}, blocking=True, @@ -182,7 +182,7 @@ async def test_event_register(hass: HomeAssistant, knx: KNXTestKit) -> None: # register event without `type` await hass.services.async_call( - "knx", "event_register", {"address": test_address}, blocking=True + DOMAIN, "event_register", {"address": test_address}, blocking=True ) await knx.receive_write(test_address, True) await knx.receive_write(test_address, False) @@ -197,7 +197,7 @@ async def test_event_register(hass: HomeAssistant, knx: KNXTestKit) -> None: # remove event for non-registered address - raise error with pytest.raises(HomeAssistantError) as exc_info: await hass.services.async_call( - "knx", + DOMAIN, "event_register", {"address": "4/4/4", "remove": True}, blocking=True, @@ -222,7 +222,7 @@ async def test_exposure_register(hass: HomeAssistant, knx: KNXTestKit) -> None: # register exposure await hass.services.async_call( - "knx", + DOMAIN, "exposure_register", {"address": test_address, "entity_id": test_entity, "type": "binary"}, blocking=True, @@ -233,7 +233,7 @@ async def test_exposure_register(hass: HomeAssistant, knx: KNXTestKit) -> None: # register exposure await hass.services.async_call( - "knx", + DOMAIN, "exposure_register", {"address": test_address, "remove": True}, blocking=True, @@ -244,7 +244,7 @@ async def test_exposure_register(hass: HomeAssistant, knx: KNXTestKit) -> None: # register exposure for attribute with default await hass.services.async_call( - "knx", + DOMAIN, "exposure_register", { "address": test_address, @@ -289,7 +289,7 @@ async def test_reload_service( patch("homeassistant.components.knx.async_setup_entry") as mock_setup_entry, ): await hass.services.async_call( - "knx", + DOMAIN, "reload", blocking=True, ) @@ -304,7 +304,7 @@ async def test_service_setup_failed(hass: HomeAssistant, knx: KNXTestKit) -> Non with pytest.raises(HomeAssistantError) as exc_info: await hass.services.async_call( - "knx", + DOMAIN, "send", {"address": "1/2/3", "payload": True, "response": False}, blocking=True, diff --git a/tests/components/knx/test_telegrams.py b/tests/components/knx/test_telegrams.py index 1ce63029a24fb9..8d2f64bc7cc730 100644 --- a/tests/components/knx/test_telegrams.py +++ b/tests/components/knx/test_telegrams.py @@ -8,6 +8,7 @@ from homeassistant.components.knx.const import ( CONF_KNX_TELEGRAM_LOG_SIZE, + DOMAIN, KNX_MODULE_KEY, ) from homeassistant.components.knx.telegrams import TelegramDict @@ -78,7 +79,7 @@ async def test_store_telegam_history( await knx.receive_write("1/3/4", True) await hass.services.async_call( - "knx", "send", {"address": "2/2/2", "payload": [1, 2, 3, 4]}, blocking=True + DOMAIN, "send", {"address": "2/2/2", "payload": [1, 2, 3, 4]}, blocking=True ) await knx.assert_write("2/2/2", (1, 2, 3, 4)) diff --git a/tests/components/knx/test_trigger.py b/tests/components/knx/test_trigger.py index 2b1836342df8a6..79a5eb90a4fe6e 100644 --- a/tests/components/knx/test_trigger.py +++ b/tests/components/knx/test_trigger.py @@ -5,6 +5,7 @@ import pytest from homeassistant.components import automation +from homeassistant.components.knx import DOMAIN from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.setup import async_setup_component @@ -242,7 +243,7 @@ async def test_telegram_trigger_options( assert len(service_calls) == 0 await hass.services.async_call( - "knx", + DOMAIN, "send", {"address": "0/0/1", "payload": True}, blocking=True, diff --git a/tests/components/light/test_reproduce_state.py b/tests/components/light/test_reproduce_state.py index 00d8b2986eb7c3..4c8243d4c44227 100644 --- a/tests/components/light/test_reproduce_state.py +++ b/tests/components/light/test_reproduce_state.py @@ -3,6 +3,7 @@ import pytest from homeassistant.components import light +from homeassistant.components.light import DOMAIN from homeassistant.core import HomeAssistant, State from homeassistant.helpers.state import async_reproduce_state @@ -39,8 +40,8 @@ async def test_reproducing_states( hass.states.async_set("light.entity_rgb", "on", VALID_RGB_COLOR) hass.states.async_set("light.entity_xy", "on", VALID_XY_COLOR) - turn_on_calls = async_mock_service(hass, "light", "turn_on") - turn_off_calls = async_mock_service(hass, "light", "turn_off") + turn_on_calls = async_mock_service(hass, DOMAIN, "turn_on") + turn_off_calls = async_mock_service(hass, DOMAIN, "turn_off") # These calls should do nothing as entities already in desired state await async_reproduce_state( @@ -155,7 +156,7 @@ async def test_filter_color_modes( **VALID_BRIGHTNESS, } - turn_on_calls = async_mock_service(hass, "light", "turn_on") + turn_on_calls = async_mock_service(hass, DOMAIN, "turn_on") await async_reproduce_state( hass, [State("light.entity", "on", {**all_colors, "color_mode": color_mode})] @@ -205,7 +206,7 @@ async def test_filter_color_modes_missing_attributes( ) expected_fallback_log = "using color_temp (mireds) as fallback" - turn_on_calls = async_mock_service(hass, "light", "turn_on") + turn_on_calls = async_mock_service(hass, DOMAIN, "turn_on") all_colors = { **VALID_COLOR_TEMP_KELVIN, @@ -261,7 +262,7 @@ async def test_filter_none(hass: HomeAssistant, saved_state) -> None: """Test filtering of parameters which are None.""" hass.states.async_set("light.entity", "off", {}) - turn_on_calls = async_mock_service(hass, "light", "turn_on") + turn_on_calls = async_mock_service(hass, DOMAIN, "turn_on") await async_reproduce_state(hass, [State("light.entity", "on", saved_state)]) diff --git a/tests/components/locative/test_init.py b/tests/components/locative/test_init.py index c41db68e3d6fea..4171784beb6c9e 100644 --- a/tests/components/locative/test_init.py +++ b/tests/components/locative/test_init.py @@ -45,7 +45,7 @@ async def webhook_id(hass: HomeAssistant, locative_client: TestClient) -> str: {"internal_url": "http://example.local:8123"}, ) result = await hass.config_entries.flow.async_init( - "locative", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM, result diff --git a/tests/components/lock/test_device_action.py b/tests/components/lock/test_device_action.py index ee1edc39eb06bd..24053bdce46ba8 100644 --- a/tests/components/lock/test_device_action.py +++ b/tests/components/lock/test_device_action.py @@ -185,9 +185,9 @@ async def test_action( ) await hass.async_block_till_done() - lock_calls = async_mock_service(hass, "lock", "lock") - unlock_calls = async_mock_service(hass, "lock", "unlock") - open_calls = async_mock_service(hass, "lock", "open") + lock_calls = async_mock_service(hass, DOMAIN, "lock") + unlock_calls = async_mock_service(hass, DOMAIN, "unlock") + open_calls = async_mock_service(hass, DOMAIN, "open") hass.bus.async_fire("test_event_lock") await hass.async_block_till_done() @@ -253,7 +253,7 @@ async def test_action_legacy( ) await hass.async_block_till_done() - lock_calls = async_mock_service(hass, "lock", "lock") + lock_calls = async_mock_service(hass, DOMAIN, "lock") hass.bus.async_fire("test_event_lock") await hass.async_block_till_done() diff --git a/tests/components/lock/test_reproduce_state.py b/tests/components/lock/test_reproduce_state.py index e501e03ebcd34f..77e225a3ab55b3 100644 --- a/tests/components/lock/test_reproduce_state.py +++ b/tests/components/lock/test_reproduce_state.py @@ -2,6 +2,7 @@ import pytest +from homeassistant.components.lock import DOMAIN from homeassistant.core import HomeAssistant, State from homeassistant.helpers.state import async_reproduce_state @@ -16,9 +17,9 @@ async def test_reproducing_states( hass.states.async_set("lock.entity_unlocked", "unlocked", {}) hass.states.async_set("lock.entity_opened", "open", {}) - lock_calls = async_mock_service(hass, "lock", "lock") - unlock_calls = async_mock_service(hass, "lock", "unlock") - open_calls = async_mock_service(hass, "lock", "open") + lock_calls = async_mock_service(hass, DOMAIN, "lock") + unlock_calls = async_mock_service(hass, DOMAIN, "unlock") + open_calls = async_mock_service(hass, DOMAIN, "open") # These calls should do nothing as entities already in desired state await async_reproduce_state( diff --git a/tests/components/logger/test_init.py b/tests/components/logger/test_init.py index 91437fda70fe1b..88bf7291ddc00b 100644 --- a/tests/components/logger/test_init.py +++ b/tests/components/logger/test_init.py @@ -9,7 +9,7 @@ import pytest from homeassistant.components import logger -from homeassistant.components.logger import LOGSEVERITY +from homeassistant.components.logger import DOMAIN, LOGSEVERITY from homeassistant.components.logger.helpers import SAVE_DELAY_LONG from homeassistant.core import Context, HomeAssistant from homeassistant.exceptions import Unauthorized @@ -140,7 +140,7 @@ async def test_setting_level(hass: HomeAssistant) -> None: # Test set default level with patch("logging.getLogger", mocks.__getitem__): await hass.services.async_call( - "logger", "set_default_level", {"level": "fatal"}, blocking=True + DOMAIN, "set_default_level", {"level": "fatal"}, blocking=True ) assert len(mocks[""].orig_setLevel.mock_calls) == 2 assert mocks[""].orig_setLevel.mock_calls[1][1][0] == LOGSEVERITY["FATAL"] @@ -148,7 +148,7 @@ async def test_setting_level(hass: HomeAssistant) -> None: # Test update other loggers with patch("logging.getLogger", mocks.__getitem__): await hass.services.async_call( - "logger", + DOMAIN, "set_level", {"test.child": "info", "new_logger": "notset"}, blocking=True, diff --git a/tests/components/mailgun/test_init.py b/tests/components/mailgun/test_init.py index 7dbde02b10f615..10c451aed9c0de 100644 --- a/tests/components/mailgun/test_init.py +++ b/tests/components/mailgun/test_init.py @@ -8,6 +8,7 @@ from homeassistant import config_entries from homeassistant.components import mailgun, webhook +from homeassistant.components.mailgun import DOMAIN from homeassistant.const import CONF_API_KEY, CONF_DOMAIN from homeassistant.core import Event, HomeAssistant, callback from homeassistant.core_config import async_process_ha_core_config @@ -42,7 +43,7 @@ async def webhook_id_with_api_key(hass: HomeAssistant) -> str: {"internal_url": "http://example.local:8123"}, ) result = await hass.config_entries.flow.async_init( - "mailgun", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM, result @@ -62,7 +63,7 @@ async def webhook_id_without_api_key(hass: HomeAssistant) -> str: {"internal_url": "http://example.local:8123"}, ) result = await hass.config_entries.flow.async_init( - "mailgun", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM, result diff --git a/tests/components/matter/test_water_heater.py b/tests/components/matter/test_water_heater.py index 3dcb9dc6511376..a92fd179fc5fdb 100644 --- a/tests/components/matter/test_water_heater.py +++ b/tests/components/matter/test_water_heater.py @@ -8,6 +8,7 @@ import pytest from syrupy.assertion import SnapshotAssertion +from homeassistant.components.matter import DOMAIN from homeassistant.components.matter.services import ( ATTR_DURATION, ATTR_EMERGENCY_BOOST, @@ -305,7 +306,7 @@ async def test_async_boost_actions( # Set boost with duration, emergency_boost, and temporary_setpoint await hass.services.async_call( - "matter", + DOMAIN, SERVICE_WATER_HEATER_BOOST, { ATTR_ENTITY_ID: "water_heater.water_heater", diff --git a/tests/components/met/test_weather.py b/tests/components/met/test_weather.py index ac3904684e319b..131457e867ddd0 100644 --- a/tests/components/met/test_weather.py +++ b/tests/components/met/test_weather.py @@ -23,7 +23,7 @@ async def test_new_config_entry( hass: HomeAssistant, entity_registry: er.EntityRegistry, mock_weather ) -> None: """Test the expected entities are created.""" - await hass.config_entries.flow.async_init("met", context={"source": "onboarding"}) + await hass.config_entries.flow.async_init(DOMAIN, context={"source": "onboarding"}) await hass.async_block_till_done() assert len(hass.states.async_entity_ids("weather")) == 1 @@ -40,7 +40,7 @@ async def test_legacy_config_entry( DOMAIN, "home-hourly", ) - await hass.config_entries.flow.async_init("met", context={"source": "onboarding"}) + await hass.config_entries.flow.async_init(DOMAIN, context={"source": "onboarding"}) await hass.async_block_till_done() assert len(hass.states.async_entity_ids("weather")) == 1 @@ -69,7 +69,7 @@ async def test_weather(hass: HomeAssistant, mock_weather) -> None: async def test_tracking_home(hass: HomeAssistant, mock_weather) -> None: """Test we track home.""" - await hass.config_entries.flow.async_init("met", context={"source": "onboarding"}) + await hass.config_entries.flow.async_init(DOMAIN, context={"source": "onboarding"}) await hass.async_block_till_done() assert len(hass.states.async_entity_ids("weather")) == 1 assert len(mock_weather.mock_calls) == 4 @@ -95,7 +95,7 @@ async def test_not_tracking_home(hass: HomeAssistant, mock_weather) -> None: """Test when we not track home.""" await hass.config_entries.flow.async_init( - "met", + DOMAIN, context={"source": config_entries.SOURCE_USER}, data={"name": "Somewhere", "latitude": 10, "longitude": 20, "elevation": 0}, ) @@ -133,7 +133,7 @@ async def test_remove_hourly_entity( ] await hass.config_entries.flow.async_init( - "met", + DOMAIN, context={"source": config_entries.SOURCE_USER}, data={"name": "Somewhere", "latitude": 10, "longitude": 20, "elevation": 0}, ) diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 4999a831511c8d..f8b3bbfa483216 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -376,7 +376,7 @@ async def test_user_connection_works( mock_try_connection.return_value = True result = await hass.config_entries.flow.async_init( - "mqtt", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM @@ -409,7 +409,7 @@ async def test_user_connection_works_with_supervisor( mock_try_connection.return_value = True result = await hass.config_entries.flow.async_init( - "mqtt", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.MENU assert result["menu_options"] == ["addon", "broker"] @@ -450,7 +450,7 @@ async def test_user_v5_connection_works( mock_try_connection.return_value = True result = await hass.config_entries.flow.async_init( - "mqtt", + DOMAIN, context={"source": config_entries.SOURCE_USER}, ) assert result["type"] is FlowResultType.FORM @@ -488,7 +488,7 @@ async def test_user_connection_fails( ) -> None: """Test if connection cannot be made.""" result = await hass.config_entries.flow.async_init( - "mqtt", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM @@ -518,7 +518,7 @@ async def test_manual_config_set( # Start config flow result = await hass.config_entries.flow.async_init( - "mqtt", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM @@ -555,7 +555,7 @@ async def test_user_single_instance(hass: HomeAssistant) -> None: ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( - "mqtt", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -570,7 +570,7 @@ async def test_hassio_already_configured(hass: HomeAssistant) -> None: ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( - "mqtt", context={"source": config_entries.SOURCE_HASSIO} + DOMAIN, context={"source": config_entries.SOURCE_HASSIO} ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" @@ -612,7 +612,7 @@ async def test_hassio_confirm( ) -> None: """Test we can finish a config flow.""" result = await hass.config_entries.flow.async_init( - "mqtt", + DOMAIN, data=HassioServiceInfo( config=ADD_ON_DISCOVERY_INFO.copy(), name="Mosquitto Mqtt Broker", @@ -652,7 +652,7 @@ async def test_hassio_cannot_connect( ) -> None: """Test a config flow is aborted when a connection was not successful.""" result = await hass.config_entries.flow.async_init( - "mqtt", + DOMAIN, data=HassioServiceInfo( config={ "addon": "Mock Addon", @@ -713,7 +713,7 @@ async def test_addon_flow_with_supervisor_addon_running( """ # show menu result = await hass.config_entries.flow.async_init( - "mqtt", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.MENU assert result["menu_options"] == ["addon", "broker"] @@ -768,7 +768,7 @@ async def test_addon_flow_with_supervisor_addon_installed( """ # show menu result = await hass.config_entries.flow.async_init( - "mqtt", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.MENU assert result["menu_options"] == ["addon", "broker"] @@ -835,7 +835,7 @@ async def test_addon_flow_with_supervisor_addon_running_connection_fails( """ # show menu result = await hass.config_entries.flow.async_init( - "mqtt", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.MENU assert result["menu_options"] == ["addon", "broker"] @@ -868,7 +868,7 @@ async def test_addon_not_running_api_error( start_addon.side_effect = SupervisorError() result = await hass.config_entries.flow.async_init( - "mqtt", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.MENU assert result["menu_options"] == ["addon", "broker"] @@ -912,7 +912,7 @@ async def test_addon_discovery_info_error( get_addon_discovery_info.side_effect = AddonError result = await hass.config_entries.flow.async_init( - "mqtt", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.MENU assert result["menu_options"] == ["addon", "broker"] @@ -955,7 +955,7 @@ async def test_addon_info_error( addon_info.side_effect = SupervisorError() result = await hass.config_entries.flow.async_init( - "mqtt", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.MENU assert result["menu_options"] == ["addon", "broker"] @@ -1002,7 +1002,7 @@ async def test_addon_flow_with_supervisor_addon_not_installed( Case: The Mosquitto add-on is not yet installed nor running. """ result = await hass.config_entries.flow.async_init( - "mqtt", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.MENU assert result["menu_options"] == ["addon", "broker"] @@ -1067,7 +1067,7 @@ async def test_addon_not_installed_failures( install_addon.side_effect = SupervisorError() result = await hass.config_entries.flow.async_init( - "mqtt", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.MENU assert result["menu_options"] == ["addon", "broker"] diff --git a/tests/components/neato/test_config_flow.py b/tests/components/neato/test_config_flow.py index 59d2900d1e8404..590c4d2f60cc4b 100644 --- a/tests/components/neato/test_config_flow.py +++ b/tests/components/neato/test_config_flow.py @@ -41,7 +41,7 @@ async def test_full_flow( ) result = await hass.config_entries.flow.async_init( - "neato", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) state = config_entry_oauth2_flow._encode_jwt( hass, @@ -93,7 +93,7 @@ async def test_abort_if_already_setup(hass: HomeAssistant) -> None: # Should fail result = await hass.config_entries.flow.async_init( - "neato", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/netatmo/test_config_flow.py b/tests/components/netatmo/test_config_flow.py index f5714d69a988af..9773b70294334f 100644 --- a/tests/components/netatmo/test_config_flow.py +++ b/tests/components/netatmo/test_config_flow.py @@ -41,13 +41,13 @@ async def test_abort_if_existing_entry(hass: HomeAssistant) -> None: flow.hass = hass result = await hass.config_entries.flow.async_init( - "netatmo", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" result = await hass.config_entries.flow.async_init( - "netatmo", + DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, data=ZeroconfServiceInfo( ip_address=ip_address("192.168.1.5"), @@ -72,7 +72,7 @@ async def test_full_flow( """Check full flow.""" result = await hass.config_entries.flow.async_init( - "netatmo", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) state = config_entry_oauth2_flow._encode_jwt( hass, @@ -239,7 +239,7 @@ async def test_reauth( """Test initialization of the reauth flow.""" result = await hass.config_entries.flow.async_init( - "netatmo", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) state = config_entry_oauth2_flow._encode_jwt( hass, diff --git a/tests/components/notify/test_legacy.py b/tests/components/notify/test_legacy.py index 2bc07239b6f6a6..e0ad2cffd51d33 100644 --- a/tests/components/notify/test_legacy.py +++ b/tests/components/notify/test_legacy.py @@ -12,6 +12,7 @@ from homeassistant import config as hass_config from homeassistant.components import notify +from homeassistant.components.notify import DOMAIN from homeassistant.const import SERVICE_RELOAD, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.discovery import async_load_platform @@ -650,7 +651,7 @@ async def test_messages_to_targets_route(hass: HomeAssistant, tmp_path: Path) -> ) await hass.services.async_call( - "notify", + DOMAIN, "test_target_name", {"message": "my message", "title": "my title", "data": {"hello": "world"}}, ) diff --git a/tests/components/onvif/__init__.py b/tests/components/onvif/__init__.py index 7e0d8cc265c84a..92cb023d0fae94 100644 --- a/tests/components/onvif/__init__.py +++ b/tests/components/onvif/__init__.py @@ -11,7 +11,7 @@ from homeassistant import config_entries from homeassistant.components.onvif import config_flow -from homeassistant.components.onvif.const import CONF_SNAPSHOT_AUTH +from homeassistant.components.onvif.const import CONF_SNAPSHOT_AUTH, DOMAIN from homeassistant.components.onvif.event_manager import EventManager from homeassistant.components.onvif.models import ( Capabilities, @@ -275,7 +275,7 @@ async def setup_onvif_integration( } config_entry = MockConfigEntry( - domain=config_flow.DOMAIN, + domain=DOMAIN, source=source, data={**config}, options=options or {}, diff --git a/tests/components/onvif/test_config_flow.py b/tests/components/onvif/test_config_flow.py index efbe3249ecba16..2030cd7e3bf7c5 100644 --- a/tests/components/onvif/test_config_flow.py +++ b/tests/components/onvif/test_config_flow.py @@ -105,7 +105,7 @@ async def test_flow_discovered_devices(hass: HomeAssistant) -> None: logging.getLogger("homeassistant.components.onvif").setLevel(logging.DEBUG) result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM @@ -178,7 +178,7 @@ async def test_flow_discovered_devices_ignore_configured_manual_input( await setup_onvif_integration(hass) result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM @@ -219,7 +219,7 @@ async def test_flow_discovered_no_device(hass: HomeAssistant) -> None: await setup_onvif_integration(hass) result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM @@ -264,7 +264,7 @@ async def test_flow_discovery_ignore_existing_and_abort(hass: HomeAssistant) -> ) result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM @@ -310,7 +310,7 @@ async def test_flow_manual_entry(hass: HomeAssistant) -> None: """Test that config flow works for discovered devices.""" logging.getLogger("homeassistant.components.onvif").setLevel(logging.DEBUG) result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM @@ -369,7 +369,7 @@ async def test_flow_manual_entry(hass: HomeAssistant) -> None: async def test_flow_manual_entry_no_profiles(hass: HomeAssistant) -> None: """Test that config flow when no profiles are returned.""" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM @@ -411,7 +411,7 @@ async def test_flow_manual_entry_no_profiles(hass: HomeAssistant) -> None: async def test_flow_manual_entry_no_mac(hass: HomeAssistant) -> None: """Test that config flow when no mac address is returned.""" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM @@ -455,7 +455,7 @@ async def test_flow_manual_entry_no_mac(hass: HomeAssistant) -> None: async def test_flow_manual_entry_fails(hass: HomeAssistant) -> None: """Test that we get a good error when manual entry fails.""" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM @@ -565,7 +565,7 @@ async def test_flow_manual_entry_fails(hass: HomeAssistant) -> None: async def test_flow_manual_entry_wrong_password(hass: HomeAssistant) -> None: """Test that we get a an auth error with the wrong password.""" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM @@ -845,7 +845,7 @@ async def test_flow_manual_entry_updates_existing_user_password( entry, _, _ = await setup_onvif_integration(hass) result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM @@ -898,7 +898,7 @@ async def test_flow_manual_entry_updates_existing_user_password( async def test_flow_manual_entry_wrong_port(hass: HomeAssistant) -> None: """Test that we get a useful error with the wrong port.""" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM diff --git a/tests/components/onvif/test_ptz.py b/tests/components/onvif/test_ptz.py index 2a2ab2938723b3..0ed6b78bfe8e2e 100644 --- a/tests/components/onvif/test_ptz.py +++ b/tests/components/onvif/test_ptz.py @@ -51,7 +51,7 @@ def _make_entry() -> MockConfigEntry: async def _call_ptz(hass: HomeAssistant, continuous_duration: float) -> None: """Call the onvif.ptz service with the given continuous_duration.""" await hass.services.async_call( - "onvif", + DOMAIN, "ptz", { "entity_id": "camera.testcamera_mainstream", diff --git a/tests/components/openai_conversation/test_init.py b/tests/components/openai_conversation/test_init.py index 829676d35cffce..f1bb3b689c3829 100644 --- a/tests/components/openai_conversation/test_init.py +++ b/tests/components/openai_conversation/test_init.py @@ -118,7 +118,7 @@ async def test_generate_image_service( ), ) as mock_create: response = await hass.services.async_call( - "openai_conversation", + DOMAIN, "generate_image", service_data, blocking=True, @@ -154,7 +154,7 @@ async def test_generate_image_service_error( pytest.raises(HomeAssistantError, match="Error generating image: Reason"), ): await hass.services.async_call( - "openai_conversation", + DOMAIN, "generate_image", { "config_entry": mock_config_entry.entry_id, @@ -182,7 +182,7 @@ async def test_generate_image_service_error( pytest.raises(HomeAssistantError, match="No image returned"), ): await hass.services.async_call( - "openai_conversation", + DOMAIN, "generate_image", { "config_entry": mock_config_entry.entry_id, @@ -212,7 +212,7 @@ async def test_generate_content_service_with_image_not_allowed_path( ), ): await hass.services.async_call( - "openai_conversation", + DOMAIN, "generate_content", { "config_entry": mock_config_entry.entry_id, @@ -245,7 +245,7 @@ async def test_invalid_config_entry( } with pytest.raises(ServiceValidationError, match=error): await hass.services.async_call( - "openai_conversation", + DOMAIN, service_name, service_data, blocking=True, @@ -473,7 +473,7 @@ async def test_generate_content_service( ) response = await hass.services.async_call( - "openai_conversation", + DOMAIN, "generate_content", service_data, blocking=True, @@ -546,7 +546,7 @@ async def test_generate_content_service_invalid( ): with pytest.raises(HomeAssistantError, match=error): await hass.services.async_call( - "openai_conversation", + DOMAIN, "generate_content", service_data, blocking=True, @@ -575,7 +575,7 @@ async def test_generate_content_service_error( pytest.raises(HomeAssistantError, match="Error generating content: Reason"), ): await hass.services.async_call( - "openai_conversation", + DOMAIN, "generate_content", { "config_entry": mock_config_entry.entry_id, @@ -615,7 +615,7 @@ async def test_service_auth_error( pytest.raises(HomeAssistantError, match="Authentication error"), ): await hass.services.async_call( - "openai_conversation", + DOMAIN, service_name, { "config_entry": mock_config_entry.entry_id, diff --git a/tests/components/overkiz/test_cover.py b/tests/components/overkiz/test_cover.py index 5359bd53ba227e..e8b7433de7f0f5 100644 --- a/tests/components/overkiz/test_cover.py +++ b/tests/components/overkiz/test_cover.py @@ -27,6 +27,7 @@ CoverEntityFeature, CoverState, ) +from homeassistant.components.overkiz import DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, STATE_UNAVAILABLE, @@ -1253,7 +1254,7 @@ async def test_set_cover_position_and_tilt_executes_single_command( await setup_overkiz_integration(fixture=DYNAMIC_EXTERIOR_VENETIAN_BLIND.fixture) await hass.services.async_call( - "overkiz", + DOMAIN, "set_cover_position_and_tilt", { ATTR_ENTITY_ID: DYNAMIC_EXTERIOR_VENETIAN_BLIND.entity_id, @@ -1294,7 +1295,7 @@ async def test_set_cover_position_and_tilt_inverts_boundaries( await setup_overkiz_integration(fixture=DYNAMIC_EXTERIOR_VENETIAN_BLIND.fixture) await hass.services.async_call( - "overkiz", + DOMAIN, "set_cover_position_and_tilt", { ATTR_ENTITY_ID: DYNAMIC_EXTERIOR_VENETIAN_BLIND.entity_id, @@ -1334,7 +1335,7 @@ async def test_set_cover_position_and_tilt_unsupported_command_raises( pytest.raises(ServiceValidationError), ): await hass.services.async_call( - "overkiz", + DOMAIN, "set_cover_position_and_tilt", { ATTR_ENTITY_ID: DYNAMIC_EXTERIOR_VENETIAN_BLIND.entity_id, diff --git a/tests/components/person/test_init.py b/tests/components/person/test_init.py index 6b4bfc7f9acf8f..679b74476e65df 100644 --- a/tests/components/person/test_init.py +++ b/tests/components/person/test_init.py @@ -1,8 +1,10 @@ """The tests for the person component.""" +from datetime import timedelta from typing import Any from unittest.mock import patch +from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components import person @@ -46,11 +48,13 @@ async def test_minimal_setup(hass: HomeAssistant) -> None: state = hass.states.get("person.test_person") assert state.state == STATE_UNKNOWN - assert state.attributes.get(ATTR_LATITUDE) is None - assert state.attributes.get(ATTR_LONGITUDE) is None - assert state.attributes.get(ATTR_SOURCE) is None - assert state.attributes.get(ATTR_USER_ID) is None - assert state.attributes.get(ATTR_ENTITY_PICTURE) is None + assert state.attributes == { + ATTR_DEVICE_TRACKERS: [], + ATTR_EDITABLE: False, + ATTR_FRIENDLY_NAME: "test person", + ATTR_ID: "1234", + ATTR_IN_ZONES: [], + } async def test_setup_no_id(hass: HomeAssistant) -> None: @@ -73,11 +77,14 @@ async def test_setup_user_id(hass: HomeAssistant, hass_admin_user: MockUser) -> state = hass.states.get("person.test_person") assert state.state == STATE_UNKNOWN - assert state.attributes.get(ATTR_ID) == "1234" - assert state.attributes.get(ATTR_LATITUDE) is None - assert state.attributes.get(ATTR_LONGITUDE) is None - assert state.attributes.get(ATTR_SOURCE) is None - assert state.attributes.get(ATTR_USER_ID) == user_id + assert state.attributes == { + ATTR_DEVICE_TRACKERS: [], + ATTR_EDITABLE: False, + ATTR_FRIENDLY_NAME: "test person", + ATTR_ID: "1234", + ATTR_IN_ZONES: [], + ATTR_USER_ID: user_id, + } async def test_valid_invalid_user_ids( @@ -95,11 +102,14 @@ async def test_valid_invalid_user_ids( state = hass.states.get("person.test_valid_user") assert state.state == STATE_UNKNOWN - assert state.attributes.get(ATTR_ID) == "1234" - assert state.attributes.get(ATTR_LATITUDE) is None - assert state.attributes.get(ATTR_LONGITUDE) is None - assert state.attributes.get(ATTR_SOURCE) is None - assert state.attributes.get(ATTR_USER_ID) == user_id + assert state.attributes == { + ATTR_DEVICE_TRACKERS: [], + ATTR_EDITABLE: False, + ATTR_FRIENDLY_NAME: "test valid user", + ATTR_ID: "1234", + ATTR_IN_ZONES: [], + ATTR_USER_ID: user_id, + } state = hass.states.get("person.test_bad_user") assert state is None @@ -211,52 +221,46 @@ async def test_setup_two_trackers( } assert await async_setup_component(hass, DOMAIN, config) + expected_attributes = { + ATTR_DEVICE_TRACKERS: [DEVICE_TRACKER, DEVICE_TRACKER_2], + ATTR_EDITABLE: False, + ATTR_FRIENDLY_NAME: "tracked person", + ATTR_ID: "1234", + ATTR_IN_ZONES: [], + ATTR_USER_ID: user_id, + } + state = hass.states.get("person.tracked_person") assert state.state == STATE_UNKNOWN - assert state.attributes.get(ATTR_ID) == "1234" - assert state.attributes.get(ATTR_LATITUDE) is None - assert state.attributes.get(ATTR_LONGITUDE) is None - assert state.attributes.get(ATTR_SOURCE) is None - assert state.attributes.get(ATTR_USER_ID) == user_id + assert state.attributes == expected_attributes hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() - # Router tracker at home with gps_accuracy — the person entity should get - # coordinates from the home zone (which has no gps_accuracy),not from the - # router tracker's attributes. - # Note: This is not a realistic test case, a router tracker would not have - # gps_accuracy, but we want to assert that the person entity uses latitude - # longitude and accuracy from the home zone, not from the state attributes - # of the device tracker. - # Router tracker at home — person gets coordinates from the home zone, - # not from the router tracker. The router tracker has gps_accuracy=99 - # and in_zones=["zone.fake"] to verify these are NOT propagated. + # Router tracker at home — the person entity gets latitude, longitude and + # accuracy from the home zone (the coordinates source), not from the router + # tracker's own attributes. `in_zones`, however, is propagated from the + # source tracker. + # Note: a router tracker would not really have gps_accuracy; it is set here + # only to assert it is NOT propagated. hass.states.async_set( DEVICE_TRACKER, "home", { ATTR_SOURCE_TYPE: SourceType.ROUTER, ATTR_GPS_ACCURACY: 99, - ATTR_IN_ZONES: ["zone.fake"], + ATTR_IN_ZONES: ["zone.home"], }, ) await hass.async_block_till_done() state = hass.states.get("person.tracked_person") assert state.state == "home" - assert state.attributes.get(ATTR_ID) == "1234" - assert state.attributes.get(ATTR_LATITUDE) == 32.87336 - assert state.attributes.get(ATTR_LONGITUDE) == -117.22743 - # GPS accuracy and in_zones come from the coordinates source (home zone), - # not from the state source (router tracker). - assert state.attributes.get(ATTR_GPS_ACCURACY) is None - assert state.attributes.get(ATTR_IN_ZONES) == [] - assert state.attributes.get(ATTR_SOURCE) == DEVICE_TRACKER - assert state.attributes.get(ATTR_USER_ID) == user_id - assert state.attributes.get(ATTR_DEVICE_TRACKERS) == [ - DEVICE_TRACKER, - DEVICE_TRACKER_2, - ] + assert state.attributes == expected_attributes | { + ATTR_IN_ZONES: ["zone.home"], + ATTR_LATITUDE: 32.87336, + ATTR_LONGITUDE: -117.22743, + ATTR_SOURCE: DEVICE_TRACKER, + } hass.states.async_set( DEVICE_TRACKER_2, @@ -277,24 +281,20 @@ async def test_setup_two_trackers( state = hass.states.get("person.tracked_person") assert state.state == "not_home" - assert state.attributes.get(ATTR_ID) == "1234" - assert state.attributes.get(ATTR_LATITUDE) == 12.123456 - assert state.attributes.get(ATTR_LONGITUDE) == 13.123456 - assert state.attributes.get(ATTR_GPS_ACCURACY) == 12 - assert state.attributes.get(ATTR_IN_ZONES) == ["zone.work"] - assert state.attributes.get(ATTR_SOURCE) == DEVICE_TRACKER_2 - assert state.attributes.get(ATTR_USER_ID) == user_id - assert state.attributes.get(ATTR_DEVICE_TRACKERS) == [ - DEVICE_TRACKER, - DEVICE_TRACKER_2, - ] + assert state.attributes == expected_attributes | { + ATTR_GPS_ACCURACY: 12, + ATTR_LATITUDE: 12.123456, + ATTR_LONGITUDE: 13.123456, + ATTR_IN_ZONES: ["zone.work"], + ATTR_SOURCE: DEVICE_TRACKER_2, + } hass.states.async_set(DEVICE_TRACKER_2, "zone1", {ATTR_SOURCE_TYPE: SourceType.GPS}) await hass.async_block_till_done() state = hass.states.get("person.tracked_person") assert state.state == "zone1" - assert state.attributes.get(ATTR_SOURCE) == DEVICE_TRACKER_2 + assert state.attributes == expected_attributes | {ATTR_SOURCE: DEVICE_TRACKER_2} hass.states.async_set(DEVICE_TRACKER, "home", {ATTR_SOURCE_TYPE: SourceType.ROUTER}) await hass.async_block_till_done() @@ -303,7 +303,11 @@ async def test_setup_two_trackers( state = hass.states.get("person.tracked_person") assert state.state == "home" - assert state.attributes.get(ATTR_SOURCE) == DEVICE_TRACKER + assert state.attributes == expected_attributes | { + ATTR_LATITUDE: 32.87336, + ATTR_LONGITUDE: -117.22743, + ATTR_SOURCE: DEVICE_TRACKER, + } async def test_setup_router_ble_trackers( @@ -326,13 +330,18 @@ async def test_setup_router_ble_trackers( } assert await async_setup_component(hass, DOMAIN, config) + expected_attributes = { + ATTR_DEVICE_TRACKERS: [DEVICE_TRACKER, DEVICE_TRACKER_2], + ATTR_EDITABLE: False, + ATTR_FRIENDLY_NAME: "tracked person", + ATTR_ID: "1234", + ATTR_IN_ZONES: [], + ATTR_USER_ID: user_id, + } + state = hass.states.get("person.tracked_person") assert state.state == STATE_UNKNOWN - assert state.attributes.get(ATTR_ID) == "1234" - assert state.attributes.get(ATTR_LATITUDE) is None - assert state.attributes.get(ATTR_LONGITUDE) is None - assert state.attributes.get(ATTR_SOURCE) is None - assert state.attributes.get(ATTR_USER_ID) == user_id + assert state.attributes == expected_attributes hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -343,16 +352,7 @@ async def test_setup_router_ble_trackers( state = hass.states.get("person.tracked_person") assert state.state == "not_home" - assert state.attributes.get(ATTR_ID) == "1234" - assert state.attributes.get(ATTR_LATITUDE) is None - assert state.attributes.get(ATTR_LONGITUDE) is None - assert state.attributes.get(ATTR_GPS_ACCURACY) is None - assert state.attributes.get(ATTR_SOURCE) == DEVICE_TRACKER - assert state.attributes.get(ATTR_USER_ID) == user_id - assert state.attributes.get(ATTR_DEVICE_TRACKERS) == [ - DEVICE_TRACKER, - DEVICE_TRACKER_2, - ] + assert state.attributes == expected_attributes | {ATTR_SOURCE: DEVICE_TRACKER} # Set the BLE tracker to the "office" zone. hass.states.async_set( @@ -371,17 +371,295 @@ async def test_setup_router_ble_trackers( # The person should be in the office. state = hass.states.get("person.tracked_person") assert state.state == "office" - assert state.attributes.get(ATTR_ID) == "1234" - assert state.attributes.get(ATTR_LATITUDE) == 12.123456 - assert state.attributes.get(ATTR_LONGITUDE) == 13.123456 - assert state.attributes.get(ATTR_GPS_ACCURACY) == 12 - assert state.attributes.get(ATTR_IN_ZONES) == ["zone.office"] - assert state.attributes.get(ATTR_SOURCE) == DEVICE_TRACKER_2 - assert state.attributes.get(ATTR_USER_ID) == user_id - assert state.attributes.get(ATTR_DEVICE_TRACKERS) == [ - DEVICE_TRACKER, - DEVICE_TRACKER_2, - ] + assert state.attributes == expected_attributes | { + ATTR_GPS_ACCURACY: 12, + ATTR_LATITUDE: 12.123456, + ATTR_LONGITUDE: 13.123456, + ATTR_IN_ZONES: ["zone.office"], + ATTR_SOURCE: DEVICE_TRACKER_2, + } + + +# Representative device tracker states for the three priority buckets used by +# `Person._update_state`, in priority order: +# 1. a non-GPS tracker reporting "home" (highest priority) +# 2. any GPS tracker, regardless of its state (middle priority) +# 3. everything else, e.g. a non-GPS scanner associated with a non-home zone +# or a non-GPS tracker reporting "not_home" (lowest priority) +# Each value is a (state, attributes) tuple passed to `hass.states.async_set`. +_ROUTER_HOME: tuple[str, dict[str, Any]] = ( + "home", + {ATTR_SOURCE_TYPE: SourceType.ROUTER, ATTR_IN_ZONES: ["zone.home"]}, +) +_ROUTER_NOT_HOME: tuple[str, dict[str, Any]] = ( + "not_home", + {ATTR_SOURCE_TYPE: SourceType.ROUTER, ATTR_IN_ZONES: []}, +) +# A scanner tracker associated with a non-home zone reports the zone's name as +# its state and lists the zone in `in_zones` (see device_tracker PR #172157). +_SCANNER_OFFICE: tuple[str, dict[str, Any]] = ( + "office", + {ATTR_SOURCE_TYPE: SourceType.ROUTER, ATTR_IN_ZONES: ["zone.office"]}, +) +_GPS_NOT_HOME: tuple[str, dict[str, Any]] = ( + "not_home", + { + ATTR_SOURCE_TYPE: SourceType.GPS, + ATTR_LATITUDE: 1.0, + ATTR_LONGITUDE: 2.0, + ATTR_GPS_ACCURACY: 5, + ATTR_IN_ZONES: [], + }, +) +_GPS_WORK: tuple[str, dict[str, Any]] = ( + "work", + { + ATTR_SOURCE_TYPE: SourceType.GPS, + ATTR_LATITUDE: 3.0, + ATTR_LONGITUDE: 4.0, + ATTR_GPS_ACCURACY: 7, + ATTR_IN_ZONES: ["zone.work"], + }, +) + + +async def _async_setup_person_two_trackers(hass: HomeAssistant, user_id: str) -> None: + """Set up a person tracked by two device trackers, with hass running.""" + hass.set_state(CoreState.not_running) + config = { + DOMAIN: { + "id": "1234", + "name": "tracked person", + "user_id": user_id, + "device_trackers": [DEVICE_TRACKER, DEVICE_TRACKER_2], + } + } + assert await async_setup_component(hass, DOMAIN, config) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + +@pytest.mark.parametrize( + ("high_priority", "low_priority", "expected_state", "expected_extra"), + [ + # A non-GPS "home" tracker outranks a GPS tracker reporting coordinates. + # Its coordinates come from the home zone (it has none of its own). + pytest.param( + _ROUTER_HOME, + _GPS_NOT_HOME, + "home", + { + ATTR_IN_ZONES: ["zone.home"], + ATTR_LATITUDE: 32.87336, + ATTR_LONGITUDE: -117.22743, + ATTR_SOURCE: DEVICE_TRACKER, + }, + id="home_beats_gps", + ), + # A non-GPS "home" tracker outranks a scanner in another zone. + pytest.param( + _ROUTER_HOME, + _SCANNER_OFFICE, + "home", + { + ATTR_IN_ZONES: ["zone.home"], + ATTR_LATITUDE: 32.87336, + ATTR_LONGITUDE: -117.22743, + ATTR_SOURCE: DEVICE_TRACKER, + }, + id="home_beats_other_zone", + ), + # A GPS tracker outranks a scanner associated with another zone. + pytest.param( + _GPS_WORK, + _SCANNER_OFFICE, + "work", + { + ATTR_GPS_ACCURACY: 7, + ATTR_LATITUDE: 3.0, + ATTR_LONGITUDE: 4.0, + ATTR_IN_ZONES: ["zone.work"], + ATTR_SOURCE: DEVICE_TRACKER, + }, + id="gps_beats_other_zone", + ), + ], +) +async def test_state_priority_overrides_recency( + hass: HomeAssistant, + hass_admin_user: MockUser, + freezer: FrozenDateTimeFactory, + high_priority: tuple[str, dict[str, Any]], + low_priority: tuple[str, dict[str, Any]], + expected_state: str, + expected_extra: dict[str, Any], +) -> None: + """Test the higher-priority bucket wins even when its state is stale. + + There is no time-based expiry: a long-stale state from a higher-priority + bucket still wins over a fresh state from a lower-priority bucket. + """ + await _async_setup_person_two_trackers(hass, hass_admin_user.id) + + # The higher-priority tracker reports first and then goes stale. + hass.states.async_set(DEVICE_TRACKER, high_priority[0], high_priority[1]) + await hass.async_block_till_done() + freezer.tick(timedelta(hours=2)) + # The lower-priority tracker reports a much more recent update. + hass.states.async_set(DEVICE_TRACKER_2, low_priority[0], low_priority[1]) + await hass.async_block_till_done() + + state = hass.states.get("person.tracked_person") + assert state.state == expected_state + assert ( + state.attributes + == { + ATTR_DEVICE_TRACKERS: [DEVICE_TRACKER, DEVICE_TRACKER_2], + ATTR_EDITABLE: False, + ATTR_FRIENDLY_NAME: "tracked person", + ATTR_ID: "1234", + ATTR_IN_ZONES: [], + ATTR_USER_ID: hass_admin_user.id, + } + | expected_extra + ) + + +@pytest.mark.parametrize( + ("older", "newer", "expected_state", "expected_extra"), + [ + # GPS bucket: the most recent GPS state wins. + pytest.param( + _GPS_WORK, + _GPS_NOT_HOME, + "not_home", + { + ATTR_GPS_ACCURACY: 5, + ATTR_LATITUDE: 1.0, + ATTR_LONGITUDE: 2.0, + ATTR_SOURCE: DEVICE_TRACKER_2, + }, + id="gps_newer_not_home", + ), + pytest.param( + _GPS_NOT_HOME, + _GPS_WORK, + "work", + { + ATTR_GPS_ACCURACY: 7, + ATTR_LATITUDE: 3.0, + ATTR_LONGITUDE: 4.0, + ATTR_IN_ZONES: ["zone.work"], + ATTR_SOURCE: DEVICE_TRACKER_2, + }, + id="gps_newer_work", + ), + # Lowest-priority bucket: a fresh scanner in another zone wins over a + # stale "not_home", and vice versa. + pytest.param( + _ROUTER_NOT_HOME, + _SCANNER_OFFICE, + "office", + {ATTR_IN_ZONES: ["zone.office"], ATTR_SOURCE: DEVICE_TRACKER_2}, + id="other_newer_office", + ), + pytest.param( + _SCANNER_OFFICE, + _ROUTER_NOT_HOME, + "not_home", + {ATTR_SOURCE: DEVICE_TRACKER_2}, + id="other_newer_not_home", + ), + # "home" bucket: the most recent "home" tracker becomes the source and + # its coordinates come from the home zone. + pytest.param( + _ROUTER_HOME, + _ROUTER_HOME, + "home", + { + ATTR_IN_ZONES: ["zone.home"], + ATTR_LATITUDE: 32.87336, + ATTR_LONGITUDE: -117.22743, + ATTR_SOURCE: DEVICE_TRACKER_2, + }, + id="home_newer", + ), + ], +) +async def test_most_recent_state_in_bucket_wins( + hass: HomeAssistant, + hass_admin_user: MockUser, + freezer: FrozenDateTimeFactory, + older: tuple[str, dict[str, Any]], + newer: tuple[str, dict[str, Any]], + expected_state: str, + expected_extra: dict[str, Any], +) -> None: + """Test that within a bucket the most recently updated state is picked.""" + await _async_setup_person_two_trackers(hass, hass_admin_user.id) + + hass.states.async_set(DEVICE_TRACKER, older[0], older[1]) + await hass.async_block_till_done() + freezer.tick(timedelta(minutes=5)) + hass.states.async_set(DEVICE_TRACKER_2, newer[0], newer[1]) + await hass.async_block_till_done() + + state = hass.states.get("person.tracked_person") + assert state.state == expected_state + # The newer tracker is the source. + assert ( + state.attributes + == { + ATTR_DEVICE_TRACKERS: [DEVICE_TRACKER, DEVICE_TRACKER_2], + ATTR_EDITABLE: False, + ATTR_FRIENDLY_NAME: "tracked person", + ATTR_ID: "1234", + ATTR_IN_ZONES: [], + ATTR_USER_ID: hass_admin_user.id, + } + | expected_extra + ) + + +async def test_scanner_associated_with_other_zone( + hass: HomeAssistant, hass_admin_user: MockUser +) -> None: + """Test a person tracked by a scanner associated with a non-home zone. + + A connected scanner associated with a non-home zone reports the zone name + and lists the zone in `in_zones`. As a non-GPS tracker not reporting "home" + it lands in the lowest-priority bucket, so it has no coordinate fallback. + """ + hass.set_state(CoreState.not_running) + user_id = hass_admin_user.id + config = { + DOMAIN: { + "id": "1234", + "name": "tracked person", + "user_id": user_id, + "device_trackers": DEVICE_TRACKER, + } + } + assert await async_setup_component(hass, DOMAIN, config) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + hass.states.async_set(DEVICE_TRACKER, _SCANNER_OFFICE[0], _SCANNER_OFFICE[1]) + await hass.async_block_till_done() + + # No coordinates: a scanner tracker provides none of its own, and as a + # lowest-priority state it gets no coordinate fallback from the home zone. + state = hass.states.get("person.tracked_person") + assert state.state == "office" + assert state.attributes == { + ATTR_DEVICE_TRACKERS: [DEVICE_TRACKER], + ATTR_EDITABLE: False, + ATTR_FRIENDLY_NAME: "tracked person", + ATTR_ID: "1234", + ATTR_IN_ZONES: ["zone.office"], + ATTR_SOURCE: DEVICE_TRACKER, + ATTR_USER_ID: user_id, + } async def test_ignore_unavailable_states( @@ -400,8 +678,18 @@ async def test_ignore_unavailable_states( } assert await async_setup_component(hass, DOMAIN, config) + expected_attributes = { + ATTR_DEVICE_TRACKERS: [DEVICE_TRACKER, DEVICE_TRACKER_2], + ATTR_EDITABLE: False, + ATTR_FRIENDLY_NAME: "tracked person", + ATTR_ID: "1234", + ATTR_IN_ZONES: [], + ATTR_USER_ID: user_id, + } + state = hass.states.get("person.tracked_person") assert state.state == STATE_UNKNOWN + assert state.attributes == expected_attributes hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -413,6 +701,7 @@ async def test_ignore_unavailable_states( # Unknown, as only 1 device tracker has a state, but we ignore that one state = hass.states.get("person.tracked_person") assert state.state == STATE_UNKNOWN + assert state.attributes == expected_attributes hass.states.async_set(DEVICE_TRACKER_2, "not_home") await hass.async_block_till_done() @@ -420,6 +709,7 @@ async def test_ignore_unavailable_states( # Take state of tracker 2 state = hass.states.get("person.tracked_person") assert state.state == "not_home" + assert state.attributes == expected_attributes | {ATTR_SOURCE: DEVICE_TRACKER_2} # state 1 is newer but ignored, keep tracker 2 state hass.states.async_set(DEVICE_TRACKER, "unknown") @@ -427,6 +717,7 @@ async def test_ignore_unavailable_states( state = hass.states.get("person.tracked_person") assert state.state == "not_home" + assert state.attributes == expected_attributes | {ATTR_SOURCE: DEVICE_TRACKER_2} async def test_restore_home_state( @@ -456,15 +747,21 @@ async def test_restore_home_state( } assert await async_setup_component(hass, DOMAIN, config) + # When restoring state the entity_id of the person will be used as source. state = hass.states.get("person.tracked_person") assert state.state == "home" - assert state.attributes.get(ATTR_ID) == "1234" - assert state.attributes.get(ATTR_LATITUDE) == 10.12346 - assert state.attributes.get(ATTR_LONGITUDE) == 11.12346 - # When restoring state the entity_id of the person will be used as source. - assert state.attributes.get(ATTR_SOURCE) == "person.tracked_person" - assert state.attributes.get(ATTR_USER_ID) == user_id - assert state.attributes.get(ATTR_ENTITY_PICTURE) == "/bla" + assert state.attributes == { + ATTR_DEVICE_TRACKERS: [DEVICE_TRACKER], + ATTR_EDITABLE: False, + ATTR_ENTITY_PICTURE: "/bla", + ATTR_FRIENDLY_NAME: "tracked person", + ATTR_ID: "1234", + ATTR_IN_ZONES: [], + ATTR_LATITUDE: 10.12346, + ATTR_LONGITUDE: 11.12346, + ATTR_SOURCE: "person.tracked_person", + ATTR_USER_ID: user_id, + } async def test_duplicate_ids(hass: HomeAssistant, hass_admin_user: MockUser) -> None: @@ -477,7 +774,7 @@ async def test_duplicate_ids(hass: HomeAssistant, hass_admin_user: MockUser) -> } assert await async_setup_component(hass, DOMAIN, config) - assert len(hass.states.async_entity_ids("person")) == 1 + assert len(hass.states.async_entity_ids(DOMAIN)) == 1 assert hass.states.get("person.test_user_1") is not None assert hass.states.get("person.test_user_2") is None @@ -502,13 +799,18 @@ async def test_load_person_storage( hass: HomeAssistant, hass_admin_user: MockUser, storage_setup ) -> None: """Test set up person from storage.""" + expected_attributes = { + ATTR_DEVICE_TRACKERS: [DEVICE_TRACKER], + ATTR_EDITABLE: True, + ATTR_FRIENDLY_NAME: "tracked person", + ATTR_ID: "1234", + ATTR_IN_ZONES: [], + ATTR_USER_ID: hass_admin_user.id, + } + state = hass.states.get("person.tracked_person") assert state.state == STATE_UNKNOWN - assert state.attributes.get(ATTR_ID) == "1234" - assert state.attributes.get(ATTR_LATITUDE) is None - assert state.attributes.get(ATTR_LONGITUDE) is None - assert state.attributes.get(ATTR_SOURCE) is None - assert state.attributes.get(ATTR_USER_ID) == hass_admin_user.id + assert state.attributes == expected_attributes hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -517,11 +819,11 @@ async def test_load_person_storage( state = hass.states.get("person.tracked_person") assert state.state == "home" - assert state.attributes.get(ATTR_ID) == "1234" - assert state.attributes.get(ATTR_LATITUDE) == 32.87336 - assert state.attributes.get(ATTR_LONGITUDE) == -117.22743 - assert state.attributes.get(ATTR_SOURCE) == DEVICE_TRACKER - assert state.attributes.get(ATTR_USER_ID) == hass_admin_user.id + assert state.attributes == expected_attributes | { + ATTR_LATITUDE: 32.87336, + ATTR_LONGITUDE: -117.22743, + ATTR_SOURCE: DEVICE_TRACKER, + } async def test_load_person_storage_two_nonlinked( @@ -550,7 +852,7 @@ async def test_load_person_storage_two_nonlinked( } await async_setup_component(hass, DOMAIN, {}) - assert len(hass.states.async_entity_ids("person")) == 2 + assert len(hass.states.async_entity_ids(DOMAIN)) == 2 assert hass.states.get("person.tracked_person_1") is not None assert hass.states.get("person.tracked_person_2") is not None @@ -731,7 +1033,7 @@ async def test_ws_delete( assert len(persons) == 0 assert resp["success"] - assert len(hass.states.async_entity_ids("person")) == 0 + assert len(hass.states.async_entity_ids(DOMAIN)) == 0 assert not entity_registry.async_is_registered("person.tracked_person") diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index 26f88d8fdae5c9..a1ef939c815e89 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -551,7 +551,7 @@ def __init__(self) -> None: # pylint: disable=super-init-not-called # Automatic setup result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] is FlowResultType.MENU @@ -568,7 +568,7 @@ def __init__(self) -> None: # pylint: disable=super-init-not-called # Manual setup result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] is FlowResultType.MENU @@ -667,7 +667,7 @@ async def test_manual_config_with_token( """Test creating via manual configuration with only token.""" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, + DOMAIN, context={"source": SOURCE_USER}, ) diff --git a/tests/components/python_script/test_init.py b/tests/components/python_script/test_init.py index 14229e836628e6..bb380a5d840ca2 100644 --- a/tests/components/python_script/test_init.py +++ b/tests/components/python_script/test_init.py @@ -42,9 +42,7 @@ async def test_setup(hass: HomeAssistant) -> None: ), patch("homeassistant.components.python_script.execute") as mock_ex, ): - await hass.services.async_call( - "python_script", "hello", {"some": "data"}, blocking=True - ) + await hass.services.async_call(DOMAIN, "hello", {"some": "data"}, blocking=True) assert len(mock_ex.mock_calls) == 1 test_hass, script, source, data = mock_ex.mock_calls[0][1] @@ -392,7 +390,7 @@ async def test_reload(hass: HomeAssistant) -> None: "homeassistant.components.python_script.glob.iglob", return_value=scripts ), ): - await hass.services.async_call("python_script", "reload", {}, blocking=True) + await hass.services.async_call(DOMAIN, "reload", {}, blocking=True) assert not hass.services.has_service("python_script", "hello") assert hass.services.has_service("python_script", "hello2") @@ -551,7 +549,7 @@ async def test_execute_with_output( create=True, ): response = await hass.services.async_call( - "python_script", + DOMAIN, "hello", {"name": "paulus"}, blocking=True, @@ -595,7 +593,7 @@ async def test_execute_no_output( create=True, ): response = await hass.services.async_call( - "python_script", + DOMAIN, "hello", {"name": "paulus"}, blocking=True, @@ -637,7 +635,7 @@ async def test_execute_wrong_output_type(hass: HomeAssistant) -> None: pytest.raises(ServiceValidationError), ): await hass.services.async_call( - "python_script", + DOMAIN, "hello", {"name": "paulus"}, blocking=True, diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index 9b2769aa629af2..5a3f5a91504f32 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -484,7 +484,7 @@ def assert_statistic_runs_equal(run1, run2): await async_wait_purge_done(hass) # run purge method - no service data, use defaults - await hass.services.async_call("recorder", "purge") + await hass.services.async_call(DOMAIN, "purge") await hass.async_block_till_done() # Small wait for recorder thread @@ -503,7 +503,7 @@ def assert_statistic_runs_equal(run1, run2): assert statistics.count() == 4 # run purge method - correct service data - await hass.services.async_call("recorder", "purge", service_data=service_data) + await hass.services.async_call(DOMAIN, "purge", service_data=service_data) await hass.async_block_till_done() # Small wait for recorder thread @@ -539,7 +539,7 @@ def assert_statistic_runs_equal(run1, run2): # run purge method - correct service data, with repack service_data["repack"] = True - await hass.services.async_call("recorder", "purge", service_data=service_data) + await hass.services.async_call(DOMAIN, "purge", service_data=service_data) await hass.async_block_till_done() await async_wait_purge_done(hass) assert ( diff --git a/tests/components/recorder/test_purge_v32_schema.py b/tests/components/recorder/test_purge_v32_schema.py index 6d6459e2b2fea2..68abef88714d7c 100644 --- a/tests/components/recorder/test_purge_v32_schema.py +++ b/tests/components/recorder/test_purge_v32_schema.py @@ -432,7 +432,7 @@ def assert_statistic_runs_equal(run1, run2): await async_wait_purge_done(hass) # run purge method - no service data, use defaults - await hass.services.async_call("recorder", "purge") + await hass.services.async_call(DOMAIN, "purge") await hass.async_block_till_done() # Small wait for recorder thread @@ -449,7 +449,7 @@ def assert_statistic_runs_equal(run1, run2): assert statistics.count() == 4 # run purge method - correct service data - await hass.services.async_call("recorder", "purge", service_data=service_data) + await hass.services.async_call(DOMAIN, "purge", service_data=service_data) await hass.async_block_till_done() # Small wait for recorder thread @@ -483,7 +483,7 @@ def assert_statistic_runs_equal(run1, run2): # run purge method - correct service data, with repack service_data["repack"] = True - await hass.services.async_call("recorder", "purge", service_data=service_data) + await hass.services.async_call(DOMAIN, "purge", service_data=service_data) await hass.async_block_till_done() await async_wait_purge_done(hass) assert ( diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index aff5e158f33439..a5ce3dbee49994 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -12,7 +12,7 @@ from homeassistant import exceptions from homeassistant.components import recorder -from homeassistant.components.recorder import Recorder, history, statistics +from homeassistant.components.recorder import DOMAIN, Recorder, history, statistics from homeassistant.components.recorder.db_schema import StatisticsShortTerm from homeassistant.components.recorder.models import ( StatisticMeanType, @@ -4471,13 +4471,13 @@ async def test_get_statistics_service( await async_recorder_block_till_done(hass) result = await hass.services.async_call( - "recorder", "get_statistics", service_args, return_response=True, blocking=True + DOMAIN, "get_statistics", service_args, return_response=True, blocking=True ) assert result == expected_result with pytest.raises(exceptions.Unauthorized): result = await hass.services.async_call( - "recorder", + DOMAIN, "get_statistics", service_args, return_response=True, @@ -4538,7 +4538,7 @@ async def test_get_statistics_service_missing_mandatory_keys( match=re.escape(f"required key not provided @ data['{missing_key}']"), ): await hass.services.async_call( - "recorder", + DOMAIN, "get_statistics", service_args, return_response=True, diff --git a/tests/components/remote/test_reproduce_state.py b/tests/components/remote/test_reproduce_state.py index 7d1954e3ce18bf..bbd330c3e12dd4 100644 --- a/tests/components/remote/test_reproduce_state.py +++ b/tests/components/remote/test_reproduce_state.py @@ -2,6 +2,7 @@ import pytest +from homeassistant.components.remote import DOMAIN from homeassistant.core import HomeAssistant, State from homeassistant.helpers.state import async_reproduce_state @@ -15,8 +16,8 @@ async def test_reproducing_states( hass.states.async_set("remote.entity_off", "off", {}) hass.states.async_set("remote.entity_on", "on", {}) - turn_on_calls = async_mock_service(hass, "remote", "turn_on") - turn_off_calls = async_mock_service(hass, "remote", "turn_off") + turn_on_calls = async_mock_service(hass, DOMAIN, "turn_on") + turn_off_calls = async_mock_service(hass, DOMAIN, "turn_off") # These calls should do nothing as entities already in desired state await async_reproduce_state( diff --git a/tests/components/rest/test_init.py b/tests/components/rest/test_init.py index 86a87a832beb6f..6bec23e822a677 100644 --- a/tests/components/rest/test_init.py +++ b/tests/components/rest/test_init.py @@ -274,7 +274,7 @@ async def test_reload(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker) with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( - "rest", + DOMAIN, SERVICE_RELOAD, {}, blocking=True, @@ -324,7 +324,7 @@ async def test_reload_and_remove_all( with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( - "rest", + DOMAIN, SERVICE_RELOAD, {}, blocking=True, @@ -369,7 +369,7 @@ async def test_reload_fails_to_read_configuration( yaml_path = get_fixture_path("configuration_invalid.notyaml", "rest") with patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path): await hass.services.async_call( - "rest", + DOMAIN, SERVICE_RELOAD, {}, blocking=True, diff --git a/tests/components/rfxtrx/test_init.py b/tests/components/rfxtrx/test_init.py index a13b742044e994..d5b975c2c53ca3 100644 --- a/tests/components/rfxtrx/test_init.py +++ b/tests/components/rfxtrx/test_init.py @@ -4,7 +4,7 @@ import RFXtrx as rfxtrxmod -from homeassistant.components.rfxtrx.const import EVENT_RFXTRX_EVENT +from homeassistant.components.rfxtrx.const import DOMAIN, EVENT_RFXTRX_EVENT from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import device_registry as dr @@ -81,7 +81,7 @@ async def test_send(hass: HomeAssistant, rfxtrx) -> None: await setup_rfx_test_cfg(hass, device="/dev/null", devices={}) await hass.services.async_call( - "rfxtrx", "send", {"event": "0a520802060101ff0f0269"}, blocking=True + DOMAIN, "send", {"event": "0a520802060101ff0f0269"}, blocking=True ) assert rfxtrx.transport.send.mock_calls == [ diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index badc2b65bf084f..1d0d6b7a179078 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -103,7 +103,7 @@ def record_call(service): assert calls[0].data["hello"] == "world" await hass.services.async_call( - "script", "test", {"greeting": "universe"}, context=context + DOMAIN, "test", {"greeting": "universe"}, context=context ) await hass.async_block_till_done() @@ -205,7 +205,7 @@ async def test_setup_with_invalid_configs( """Test setup with invalid configs.""" assert await async_setup_component(hass, "script", {"script": config}) - assert len(hass.states.async_entity_ids("script")) == nbr_script_entities + assert len(hass.states.async_entity_ids(DOMAIN)) == nbr_script_entities @pytest.mark.parametrize( @@ -261,7 +261,7 @@ async def test_bad_config_validation_critical( ) # Make sure one bad script does not prevent other scripts from setting up - assert hass.states.async_entity_ids("script") == ["script.good_script"] + assert hass.states.async_entity_ids(DOMAIN) == ["script.good_script"] @pytest.mark.parametrize( @@ -337,7 +337,7 @@ async def test_bad_config_validation( assert issues[0]["translation_placeholders"]["error"].startswith(details) # Make sure both scripts are setup - assert set(hass.states.async_entity_ids("script")) == { + assert set(hass.states.async_entity_ids(DOMAIN)) == { "script.bad_script", "script.good_script", } @@ -677,7 +677,7 @@ async def test_logging_script_error( {"script": {"hello": {"sequence": [{"action": "non.existing"}]}}}, ) with pytest.raises(ServiceNotFound) as err: - await hass.services.async_call("script", "hello", blocking=True) + await hass.services.async_call(DOMAIN, "hello", blocking=True) assert err.value.domain == "non" assert err.value.service == "existing" @@ -1086,7 +1086,7 @@ async def async_service_handler(service): hass.states.async_set("input_boolean.test1", "off") hass.states.async_set("input_boolean.test2", "off") - await hass.services.async_call("script", "script1") + await hass.services.async_call(DOMAIN, "script1") await asyncio.wait_for(service_called.wait(), 1) service_called.clear() @@ -1183,7 +1183,7 @@ async def test_script_variables( mock_calls = async_mock_service(hass, "test", "script") await hass.services.async_call( - "script", "script1", {"var_from_service": "hello"}, blocking=True + DOMAIN, "script1", {"var_from_service": "hello"}, blocking=True ) assert len(mock_calls) == 1 @@ -1195,7 +1195,7 @@ async def test_script_variables( assert mock_calls[0].data.get("this_variable") == "script.script1" await hass.services.async_call( - "script", "script1", {"test_var": "from_service"}, blocking=True + DOMAIN, "script1", {"test_var": "from_service"}, blocking=True ) assert len(mock_calls) == 2 @@ -1204,7 +1204,7 @@ async def test_script_variables( # Call script with vars but no templates in it await hass.services.async_call( - "script", "script2", {"test_var": "from_service"}, blocking=True + DOMAIN, "script2", {"test_var": "from_service"}, blocking=True ) assert len(mock_calls) == 3 @@ -1212,11 +1212,11 @@ async def test_script_variables( assert "Error rendering variables" not in caplog.text with pytest.raises(TemplateError): - await hass.services.async_call("script", "script3", blocking=True) + await hass.services.async_call(DOMAIN, "script3", blocking=True) assert "Error rendering variables" in caplog.text assert len(mock_calls) == 3 - await hass.services.async_call("script", "script3", {"break": 0}, blocking=True) + await hass.services.async_call(DOMAIN, "script3", {"break": 0}, blocking=True) assert len(mock_calls) == 4 assert mock_calls[3].data["value"] == 1 @@ -1247,7 +1247,7 @@ async def test_script_this_var_always( ) mock_calls = async_mock_service(hass, "test", "script") - await hass.services.async_call("script", "script1", blocking=True) + await hass.services.async_call(DOMAIN, "script1", blocking=True) assert len(mock_calls) == 1 # Verify this available to all templates @@ -1335,7 +1335,7 @@ async def async_service_handler(service): hass.services.async_register("test", "script", async_service_handler) - await hass.services.async_call("script", "script1") + await hass.services.async_call(DOMAIN, "script1") await asyncio.wait_for(service_called.wait(), 1) assert warning_msg in caplog.text @@ -1403,7 +1403,7 @@ async def async_service_handler(service): hass.services.async_register("test", "script", async_service_handler) - await hass.services.async_call("script", "script1") + await hass.services.async_call(DOMAIN, "script1") await asyncio.wait_for(service_called.wait(), 1) assert warning_msg in caplog.text @@ -1482,7 +1482,7 @@ async def async_service_handler(service): hass.services.async_register("test", "script_done", async_service_handler) - await hass.services.async_call("script", "script1") + await hass.services.async_call(DOMAIN, "script1") await asyncio.wait_for(service_called.wait(), 1) # Trigger 1st stage script shutdown @@ -1518,7 +1518,7 @@ async def test_setup_with_duplicate_scripts( }, ) assert "Duplicate script detected with name: 'duplicate'" in caplog.text - assert len(hass.states.async_entity_ids("script")) == 1 + assert len(hass.states.async_entity_ids(DOMAIN)) == 1 async def test_websocket_config( @@ -1640,7 +1640,7 @@ async def test_blueprint_script(hass: HomeAssistant, calls: list[ServiceCall]) - }, ) await hass.services.async_call( - "script", "test_script", {"var_from_service": "hello"}, blocking=True + DOMAIN, "test_script", {"var_from_service": "hello"}, blocking=True ) await hass.async_block_till_done() assert len(calls) == 1 @@ -1874,7 +1874,7 @@ async def async_service_handler(*args, **kwargs) -> None: ) await hass.async_block_till_done() - await hass.services.async_call("script", "test_main", blocking=True) + await hass.services.async_call(DOMAIN, "test_main", blocking=True) assert calls == 4 diff --git a/tests/components/senz/test_config_flow.py b/tests/components/senz/test_config_flow.py index 8d6e8d3b8d6edc..38579b64446815 100644 --- a/tests/components/senz/test_config_flow.py +++ b/tests/components/senz/test_config_flow.py @@ -42,7 +42,7 @@ async def test_full_flow( ) result = await hass.config_entries.flow.async_init( - "senz", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) state = config_entry_oauth2_flow._encode_jwt( hass, diff --git a/tests/components/shell_command/test_init.py b/tests/components/shell_command/test_init.py index a534fd381eeaff..687e496d4a711d 100644 --- a/tests/components/shell_command/test_init.py +++ b/tests/components/shell_command/test_init.py @@ -11,6 +11,7 @@ import pytest from homeassistant.components import shell_command +from homeassistant.components.shell_command import DOMAIN from homeassistant.const import SERVICE_RELOAD from homeassistant.core import Context, HomeAssistant from homeassistant.exceptions import HomeAssistantError, TemplateError @@ -47,7 +48,7 @@ async def test_executing_service(hass: HomeAssistant) -> None: ) await hass.async_block_till_done() - await hass.services.async_call("shell_command", "test_service", blocking=True) + await hass.services.async_call(DOMAIN, "test_service", blocking=True) await hass.async_block_till_done() assert os.path.isfile(path) @@ -82,7 +83,7 @@ async def test_template_render_no_template(mock_call, hass: HomeAssistant) -> No ) await hass.async_block_till_done() - await hass.services.async_call("shell_command", "test_service", blocking=True) + await hass.services.async_call(DOMAIN, "test_service", blocking=True) await hass.async_block_till_done() cmd = mock_call.mock_calls[0][1][0] @@ -106,7 +107,7 @@ async def test_incorrect_template(mock_call, hass: HomeAssistant) -> None: with pytest.raises(TemplateError): await hass.services.async_call( - "shell_command", "test_service", blocking=True, return_response=True + DOMAIN, "test_service", blocking=True, return_response=True ) await hass.async_block_till_done() @@ -127,7 +128,7 @@ async def test_template_render(mock_call, hass: HomeAssistant) -> None: }, ) - await hass.services.async_call("shell_command", "test_service", blocking=True) + await hass.services.async_call(DOMAIN, "test_service", blocking=True) await hass.async_block_till_done() cmd = mock_call.mock_calls[0][1] @@ -150,7 +151,7 @@ async def test_subprocess_error(mock_error, mock_call, hass: HomeAssistant) -> N ) response = await hass.services.async_call( - "shell_command", "test_service", blocking=True, return_response=True + DOMAIN, "test_service", blocking=True, return_response=True ) await hass.async_block_till_done() assert mock_call.call_count == 1 @@ -170,7 +171,7 @@ async def test_stdout_captured(mock_output, hass: HomeAssistant) -> None: ) response = await hass.services.async_call( - "shell_command", "test_service", blocking=True, return_response=True + DOMAIN, "test_service", blocking=True, return_response=True ) await hass.async_block_till_done() @@ -200,9 +201,7 @@ async def test_non_text_stdout_capture( ) # No problem without 'return_response' - response = await hass.services.async_call( - "shell_command", "output_image", blocking=True - ) + response = await hass.services.async_call(DOMAIN, "output_image", blocking=True) await hass.async_block_till_done() assert not response @@ -215,7 +214,7 @@ async def test_non_text_stdout_capture( ), ): response = await hass.services.async_call( - "shell_command", "output_image", blocking=True, return_response=True + DOMAIN, "output_image", blocking=True, return_response=True ) await hass.async_block_till_done() @@ -234,7 +233,7 @@ async def test_stderr_captured(mock_output, hass: HomeAssistant) -> None: ) response = await hass.services.async_call( - "shell_command", "test_service", blocking=True, return_response=True + DOMAIN, "test_service", blocking=True, return_response=True ) await hass.async_block_till_done() diff --git a/tests/components/starline/test_config_flow.py b/tests/components/starline/test_config_flow.py index 0c43820d31f510..fd2a4f31618497 100644 --- a/tests/components/starline/test_config_flow.py +++ b/tests/components/starline/test_config_flow.py @@ -3,7 +3,7 @@ import requests_mock from homeassistant import config_entries -from homeassistant.components.starline import config_flow +from homeassistant.components.starline import DOMAIN, config_flow from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -54,7 +54,7 @@ async def test_flow_works(hass: HomeAssistant) -> None: ) result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "auth_app" @@ -87,7 +87,7 @@ async def test_step_auth_app_code_falls(hass: HomeAssistant) -> None: "https://id.starline.ru/apiV3/application/getCode/", text='{"state": 0}}' ) result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, + DOMAIN, context={"source": config_entries.SOURCE_USER}, data={ config_flow.CONF_APP_ID: TEST_APP_ID, @@ -110,7 +110,7 @@ async def test_step_auth_app_token_falls(hass: HomeAssistant) -> None: "https://id.starline.ru/apiV3/application/getToken/", text='{"state": 0}' ) result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, + DOMAIN, context={"source": config_entries.SOURCE_USER}, data={ config_flow.CONF_APP_ID: TEST_APP_ID, diff --git a/tests/components/subaru/test_config_flow.py b/tests/components/subaru/test_config_flow.py index 0b45546902b31d..f5b0a1354426e3 100644 --- a/tests/components/subaru/test_config_flow.py +++ b/tests/components/subaru/test_config_flow.py @@ -395,7 +395,7 @@ async def test_option_flow(hass: HomeAssistant, options_form) -> None: async def user_form(hass: HomeAssistant) -> ConfigFlowResult: """Return initial form for Subaru config flow.""" return await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) diff --git a/tests/components/swiss_public_transport/test_config_flow.py b/tests/components/swiss_public_transport/test_config_flow.py index b8d50a4e32c9c3..5253a79dab2009 100644 --- a/tests/components/swiss_public_transport/test_config_flow.py +++ b/tests/components/swiss_public_transport/test_config_flow.py @@ -17,6 +17,7 @@ CONF_TIME_OFFSET, CONF_TIME_STATION, CONF_VIA, + DOMAIN, MAX_VIA, ) from homeassistant.components.swiss_public_transport.helper import unique_id_from_config @@ -111,7 +112,7 @@ async def test_flow_user_init_data_success( ) -> None: """Test success response.""" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} + DOMAIN, context={"source": "user"} ) assert result["type"] is FlowResultType.FORM @@ -160,7 +161,7 @@ async def test_flow_user_init_data_error_and_recover_on_step_1( ) -> None: """Test errors in user step.""" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} + DOMAIN, context={"source": "user"} ) with patch( "homeassistant.components.swiss_public_transport.config_flow.OpendataTransport.async_get_data", @@ -206,7 +207,7 @@ async def test_flow_user_init_data_error_and_recover_on_step_2( ) -> None: """Test errors in time mode step.""" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} + DOMAIN, context={"source": "user"} ) assert result["type"] is FlowResultType.FORM @@ -255,7 +256,7 @@ async def test_flow_user_init_data_already_configured(hass: HomeAssistant) -> No """Test we abort user data set when entry is already configured.""" entry = MockConfigEntry( - domain=config_flow.DOMAIN, + domain=DOMAIN, data=MOCK_USER_DATA_STEP, unique_id=unique_id_from_config(MOCK_USER_DATA_STEP), ) @@ -267,7 +268,7 @@ async def test_flow_user_init_data_already_configured(hass: HomeAssistant) -> No return_value=True, ): result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": "user"} + DOMAIN, context={"source": "user"} ) result = await hass.config_entries.flow.async_configure( diff --git a/tests/components/swisscom/__init__.py b/tests/components/swisscom/__init__.py new file mode 100644 index 00000000000000..1d6f0cba9aa3f2 --- /dev/null +++ b/tests/components/swisscom/__init__.py @@ -0,0 +1 @@ +"""Tests for the Swisscom Internet-Box integration.""" diff --git a/tests/components/swisscom/conftest.py b/tests/components/swisscom/conftest.py new file mode 100644 index 00000000000000..c11d731f32eb0e --- /dev/null +++ b/tests/components/swisscom/conftest.py @@ -0,0 +1,49 @@ +"""Fixtures for the Swisscom Internet-Box integration tests.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from homeassistant.components.swisscom.const import DOMAIN + +from .const import TEST_BASE_MAC, TEST_FORMATTED_MAC, TEST_MODEL_NAME, USER_INPUT + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Return the default mocked config entry.""" + return MockConfigEntry( + title=TEST_MODEL_NAME, + domain=DOMAIN, + data=USER_INPUT, + unique_id=TEST_FORMATTED_MAC, + ) + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.swisscom.async_setup_entry", return_value=True + ) as mock_fn: + yield mock_fn + + +@pytest.fixture +def mock_swisscom_client() -> Generator[MagicMock]: + """Mock the SwisscomClient used in the config flow.""" + box_info = MagicMock() + box_info.base_mac = TEST_BASE_MAC + box_info.model_name = TEST_MODEL_NAME + + with patch( + "homeassistant.components.swisscom.config_flow.SwisscomClient", + autospec=True, + ) as mock_cls: + client = mock_cls.return_value + client.login = AsyncMock() + client.get_box_info = AsyncMock(return_value=box_info) + yield client diff --git a/tests/components/swisscom/const.py b/tests/components/swisscom/const.py new file mode 100644 index 00000000000000..2f23967f7f05da --- /dev/null +++ b/tests/components/swisscom/const.py @@ -0,0 +1,16 @@ +"""Constants for the Swisscom Internet-Box integration tests.""" + +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME + +TEST_HOST = "192.168.1.1" +TEST_USERNAME = "admin" +TEST_PASSWORD = "test-password" +TEST_BASE_MAC = "AA:BB:CC:DD:EE:FF" +TEST_FORMATTED_MAC = "aa:bb:cc:dd:ee:ff" +TEST_MODEL_NAME = "Internet-Box plus" + +USER_INPUT = { + CONF_HOST: TEST_HOST, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, +} diff --git a/tests/components/swisscom/test_config_flow.py b/tests/components/swisscom/test_config_flow.py new file mode 100644 index 00000000000000..c2152784610e59 --- /dev/null +++ b/tests/components/swisscom/test_config_flow.py @@ -0,0 +1,112 @@ +"""Tests for the Swisscom Internet-Box config flow.""" + +from unittest.mock import MagicMock + +import pytest +from swisscom_internet_box import SwisscomAuthError, SwisscomConnectionError + +from homeassistant.components.swisscom.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from .const import TEST_FORMATTED_MAC, TEST_MODEL_NAME, USER_INPUT + +from tests.common import MockConfigEntry + + +@pytest.mark.usefixtures("mock_setup_entry") +async def test_user_flow_success( + hass: HomeAssistant, mock_swisscom_client: MagicMock +) -> None: + """Test a successful user-initiated config flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=USER_INPUT + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == TEST_MODEL_NAME + assert result["data"] == USER_INPUT + assert result["result"].unique_id == TEST_FORMATTED_MAC + + +@pytest.mark.parametrize( + ("side_effect", "expected_error"), + [ + (SwisscomAuthError("bad creds"), "invalid_auth"), + (SwisscomConnectionError("unreachable"), "cannot_connect"), + (RuntimeError("boom"), "unknown"), + ], + ids=["invalid_auth", "cannot_connect", "unknown"], +) +@pytest.mark.usefixtures("mock_setup_entry") +async def test_user_flow_error_and_recovery( + hass: HomeAssistant, + mock_swisscom_client: MagicMock, + side_effect: Exception, + expected_error: str, +) -> None: + """Test user flow shows the correct error and the user can retry.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + mock_swisscom_client.login.side_effect = side_effect + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=USER_INPUT + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": expected_error} + + mock_swisscom_client.login.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=USER_INPUT + ) + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == TEST_MODEL_NAME + assert result["data"] == USER_INPUT + assert result["result"].unique_id == TEST_FORMATTED_MAC + + +async def test_user_flow_duplicate( + hass: HomeAssistant, + mock_swisscom_client: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test that duplicate boxes are rejected.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=USER_INPUT + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.usefixtures("mock_setup_entry") +async def test_user_flow_no_model_name_uses_default_title( + hass: HomeAssistant, mock_swisscom_client: MagicMock +) -> None: + """Test the entry falls back to a default title when the box reports no model.""" + mock_swisscom_client.get_box_info.return_value.model_name = "" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=USER_INPUT + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Internet-Box" diff --git a/tests/components/switch/test_reproduce_state.py b/tests/components/switch/test_reproduce_state.py index 27bc492494c9f9..e27219303ee626 100644 --- a/tests/components/switch/test_reproduce_state.py +++ b/tests/components/switch/test_reproduce_state.py @@ -2,6 +2,7 @@ import pytest +from homeassistant.components.switch import DOMAIN from homeassistant.core import HomeAssistant, State from homeassistant.helpers.state import async_reproduce_state @@ -15,8 +16,8 @@ async def test_reproducing_states( hass.states.async_set("switch.entity_off", "off", {}) hass.states.async_set("switch.entity_on", "on", {}) - turn_on_calls = async_mock_service(hass, "switch", "turn_on") - turn_off_calls = async_mock_service(hass, "switch", "turn_off") + turn_on_calls = async_mock_service(hass, DOMAIN, "turn_on") + turn_off_calls = async_mock_service(hass, DOMAIN, "turn_off") # These calls should do nothing as entities already in desired state await async_reproduce_state( diff --git a/tests/components/tasmota/test_config_flow.py b/tests/components/tasmota/test_config_flow.py index 86efd91a553d2e..41d349ea3f244d 100644 --- a/tests/components/tasmota/test_config_flow.py +++ b/tests/components/tasmota/test_config_flow.py @@ -19,7 +19,7 @@ async def test_mqtt_abort_if_existing_entry( MockConfigEntry(domain=DOMAIN).add_to_hass(hass) result = await hass.config_entries.flow.async_init( - "tasmota", context={"source": config_entries.SOURCE_MQTT} + DOMAIN, context={"source": config_entries.SOURCE_MQTT} ) assert result["type"] is FlowResultType.ABORT @@ -50,7 +50,7 @@ async def test_mqtt_abort_invalid_topic( timestamp=None, ) result = await hass.config_entries.flow.async_init( - "tasmota", context={"source": config_entries.SOURCE_MQTT}, data=discovery_info + DOMAIN, context={"source": config_entries.SOURCE_MQTT}, data=discovery_info ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_discovery_info" @@ -64,7 +64,7 @@ async def test_mqtt_abort_invalid_topic( timestamp=None, ) result = await hass.config_entries.flow.async_init( - "tasmota", context={"source": config_entries.SOURCE_MQTT}, data=discovery_info + DOMAIN, context={"source": config_entries.SOURCE_MQTT}, data=discovery_info ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "invalid_discovery_info" @@ -89,7 +89,7 @@ async def test_mqtt_abort_invalid_topic( timestamp=None, ) result = await hass.config_entries.flow.async_init( - "tasmota", context={"source": config_entries.SOURCE_MQTT}, data=discovery_info + DOMAIN, context={"source": config_entries.SOURCE_MQTT}, data=discovery_info ) assert result["type"] is FlowResultType.FORM @@ -116,7 +116,7 @@ async def test_mqtt_setup(hass: HomeAssistant, mqtt_mock: MqttMockHAClient) -> N timestamp=None, ) result = await hass.config_entries.flow.async_init( - "tasmota", context={"source": config_entries.SOURCE_MQTT}, data=discovery_info + DOMAIN, context={"source": config_entries.SOURCE_MQTT}, data=discovery_info ) assert result["type"] is FlowResultType.FORM @@ -150,7 +150,7 @@ async def test_user_setup( ) -> None: """Test we can finish a config flow.""" result = await hass.config_entries.flow.async_init( - "tasmota", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "config" @@ -168,7 +168,7 @@ async def test_user_setup_invalid_topic_prefix( ) -> None: """Test abort on invalid discovery topic.""" result = await hass.config_entries.flow.async_init( - "tasmota", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "config" @@ -188,7 +188,7 @@ async def test_user_single_instance( MockConfigEntry(domain=DOMAIN).add_to_hass(hass) result = await hass.config_entries.flow.async_init( - "tasmota", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" diff --git a/tests/components/template/test_entity.py b/tests/components/template/test_entity.py index 680db0ec4a19ca..f2fd2d12c6baf8 100644 --- a/tests/components/template/test_entity.py +++ b/tests/components/template/test_entity.py @@ -5,7 +5,7 @@ import pytest -from homeassistant.components.template import entity as abstract_entity +from homeassistant.components.template import DOMAIN, entity as abstract_entity from homeassistant.const import SERVICE_RELOAD from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -78,7 +78,7 @@ async def test_reload_stops_entity_action_scripts( autospec=True, return_value={"template": []}, ): - await hass.services.async_call("template", SERVICE_RELOAD, blocking=True) + await hass.services.async_call(DOMAIN, SERVICE_RELOAD, blocking=True) await hass.async_block_till_done() assert not turn_on_script.is_running diff --git a/tests/components/template/test_trigger_entity.py b/tests/components/template/test_trigger_entity.py index 5a874e2fdb578c..d3a42b3ccd1909 100644 --- a/tests/components/template/test_trigger_entity.py +++ b/tests/components/template/test_trigger_entity.py @@ -360,7 +360,7 @@ async def test_reload_stops_script_and_unsubscribes_triggers( autospec=True, return_value={"template": []}, ): - await hass.services.async_call("template", SERVICE_RELOAD, blocking=True) + await hass.services.async_call(DOMAIN, SERVICE_RELOAD, blocking=True) await hass.async_block_till_done() # Script should be stopped and unloaded diff --git a/tests/components/timer/test_reproduce_state.py b/tests/components/timer/test_reproduce_state.py index fcc331fef508ea..36ff55edc98ec2 100644 --- a/tests/components/timer/test_reproduce_state.py +++ b/tests/components/timer/test_reproduce_state.py @@ -4,6 +4,7 @@ from homeassistant.components.timer import ( ATTR_DURATION, + DOMAIN, SERVICE_CANCEL, SERVICE_PAUSE, SERVICE_START, @@ -28,9 +29,9 @@ async def test_reproducing_states( "timer.entity_active_attr", STATUS_ACTIVE, {ATTR_DURATION: "00:01:00"} ) - start_calls = async_mock_service(hass, "timer", SERVICE_START) - pause_calls = async_mock_service(hass, "timer", SERVICE_PAUSE) - cancel_calls = async_mock_service(hass, "timer", SERVICE_CANCEL) + start_calls = async_mock_service(hass, DOMAIN, SERVICE_START) + pause_calls = async_mock_service(hass, DOMAIN, SERVICE_PAUSE) + cancel_calls = async_mock_service(hass, DOMAIN, SERVICE_CANCEL) # These calls should do nothing as entities already in desired state await async_reproduce_state( diff --git a/tests/components/tradfri/test_config_flow.py b/tests/components/tradfri/test_config_flow.py index 6d6215a21abcdb..f3af3cc6b7de21 100644 --- a/tests/components/tradfri/test_config_flow.py +++ b/tests/components/tradfri/test_config_flow.py @@ -6,7 +6,7 @@ import pytest from homeassistant import config_entries -from homeassistant.components.tradfri import config_flow +from homeassistant.components.tradfri import DOMAIN, config_flow from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers.service_info.zeroconf import ( @@ -36,7 +36,7 @@ async def test_already_paired(hass: HomeAssistant, mock_entry_setup) -> None: mock_it.generate_psk.return_value = None mock_lib.init.return_value = mock_it result = await hass.config_entries.flow.async_init( - "tradfri", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "123.123.123.123", "security_code": "abcd"} @@ -53,7 +53,7 @@ async def test_user_connection_successful( mock_auth.side_effect = lambda hass, host, code: {"host": host, "gateway_id": "bla"} flow = await hass.config_entries.flow.async_init( - "tradfri", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( @@ -76,7 +76,7 @@ async def test_user_connection_timeout( mock_auth.side_effect = config_flow.AuthError("timeout") flow = await hass.config_entries.flow.async_init( - "tradfri", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( @@ -96,7 +96,7 @@ async def test_user_connection_bad_key( mock_auth.side_effect = config_flow.AuthError("invalid_security_code") flow = await hass.config_entries.flow.async_init( - "tradfri", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( @@ -116,7 +116,7 @@ async def test_discovery_connection( mock_auth.side_effect = lambda hass, host, code: {"host": host, "gateway_id": "bla"} flow = await hass.config_entries.flow.async_init( - "tradfri", + DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, data=ZeroconfServiceInfo( ip_address=ip_address("123.123.123.123"), @@ -151,7 +151,7 @@ async def test_discovery_duplicate_aborted(hass: HomeAssistant) -> None: entry.add_to_hass(hass) flow = await hass.config_entries.flow.async_init( - "tradfri", + DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, data=ZeroconfServiceInfo( ip_address=ip_address("123.123.123.124"), @@ -175,7 +175,7 @@ async def test_duplicate_discovery( ) -> None: """Test a duplicate discovery in progress is ignored.""" result = await hass.config_entries.flow.async_init( - "tradfri", + DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, data=ZeroconfServiceInfo( ip_address=ip_address("123.123.123.123"), @@ -191,7 +191,7 @@ async def test_duplicate_discovery( assert result["type"] is FlowResultType.FORM result2 = await hass.config_entries.flow.async_init( - "tradfri", + DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, data=ZeroconfServiceInfo( ip_address=ip_address("123.123.123.123"), @@ -216,7 +216,7 @@ async def test_discovery_updates_unique_id(hass: HomeAssistant) -> None: entry.add_to_hass(hass) flow = await hass.config_entries.flow.async_init( - "tradfri", + DOMAIN, context={"source": config_entries.SOURCE_HOMEKIT}, data=ZeroconfServiceInfo( ip_address=ip_address("123.123.123.123"), diff --git a/tests/components/twentemilieu/test_config_flow.py b/tests/components/twentemilieu/test_config_flow.py index 6dc261b87693bb..8200b0b2192eeb 100644 --- a/tests/components/twentemilieu/test_config_flow.py +++ b/tests/components/twentemilieu/test_config_flow.py @@ -6,7 +6,6 @@ from twentemilieu import TwenteMilieuAddressError, TwenteMilieuConnectionError from homeassistant import config_entries -from homeassistant.components.twentemilieu import config_flow from homeassistant.components.twentemilieu.const import ( CONF_HOUSE_LETTER, CONF_HOUSE_NUMBER, @@ -160,7 +159,7 @@ async def test_address_already_set_up( """Test we abort if address has already been set up.""" mock_config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, + DOMAIN, context={"source": config_entries.SOURCE_USER}, data={ CONF_POST_CODE: "1234AB", diff --git a/tests/components/twilio/test_init.py b/tests/components/twilio/test_init.py index 9c07bd6f3d89ae..6aa811386d37f3 100644 --- a/tests/components/twilio/test_init.py +++ b/tests/components/twilio/test_init.py @@ -2,6 +2,7 @@ from homeassistant import config_entries from homeassistant.components import twilio +from homeassistant.components.twilio import DOMAIN from homeassistant.core import HomeAssistant, callback from homeassistant.core_config import async_process_ha_core_config from homeassistant.data_entry_flow import FlowResultType @@ -18,7 +19,7 @@ async def test_config_flow_registers_webhook( {"internal_url": "http://example.local:8123"}, ) result = await hass.config_entries.flow.async_init( - "twilio", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.FORM, result diff --git a/tests/components/twitch/test_config_flow.py b/tests/components/twitch/test_config_flow.py index 5e99e2a11c52ff..36b9d63696a9d4 100644 --- a/tests/components/twitch/test_config_flow.py +++ b/tests/components/twitch/test_config_flow.py @@ -57,7 +57,7 @@ async def test_full_flow( ) -> None: """Check full flow.""" result = await hass.config_entries.flow.async_init( - "twitch", context={"source": SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) await _do_get_token(hass, result, hass_client_no_auth, scopes) @@ -86,7 +86,7 @@ async def test_already_configured( """Check flow aborts when account already configured.""" await setup_integration(hass, config_entry) result = await hass.config_entries.flow.async_init( - "twitch", context={"source": SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) await _do_get_token(hass, result, hass_client_no_auth, scopes) diff --git a/tests/components/vacuum/test_device_action.py b/tests/components/vacuum/test_device_action.py index 16eed8494395c7..b8c45354f095b2 100644 --- a/tests/components/vacuum/test_device_action.py +++ b/tests/components/vacuum/test_device_action.py @@ -141,8 +141,8 @@ async def test_action( }, ) - dock_calls = async_mock_service(hass, "vacuum", "return_to_base") - clean_calls = async_mock_service(hass, "vacuum", "start") + dock_calls = async_mock_service(hass, DOMAIN, "return_to_base") + clean_calls = async_mock_service(hass, DOMAIN, "start") hass.bus.async_fire("test_event_dock") await hass.async_block_till_done() @@ -192,7 +192,7 @@ async def test_action_legacy( }, ) - dock_calls = async_mock_service(hass, "vacuum", "return_to_base") + dock_calls = async_mock_service(hass, DOMAIN, "return_to_base") hass.bus.async_fire("test_event_dock") await hass.async_block_till_done() diff --git a/tests/components/vacuum/test_reproduce_state.py b/tests/components/vacuum/test_reproduce_state.py index dc5d81e8f0813f..ac87d78d19e064 100644 --- a/tests/components/vacuum/test_reproduce_state.py +++ b/tests/components/vacuum/test_reproduce_state.py @@ -4,6 +4,7 @@ from homeassistant.components.vacuum import ( ATTR_FAN_SPEED, + DOMAIN, SERVICE_PAUSE, SERVICE_RETURN_TO_BASE, SERVICE_SET_FAN_SPEED, @@ -36,13 +37,13 @@ async def test_reproducing_states( hass.states.async_set("vacuum.entity_returning", VacuumActivity.RETURNING, {}) hass.states.async_set("vacuum.entity_paused", VacuumActivity.PAUSED, {}) - turn_on_calls = async_mock_service(hass, "vacuum", SERVICE_TURN_ON) - turn_off_calls = async_mock_service(hass, "vacuum", SERVICE_TURN_OFF) - start_calls = async_mock_service(hass, "vacuum", SERVICE_START) - pause_calls = async_mock_service(hass, "vacuum", SERVICE_PAUSE) - stop_calls = async_mock_service(hass, "vacuum", SERVICE_STOP) - return_calls = async_mock_service(hass, "vacuum", SERVICE_RETURN_TO_BASE) - fan_speed_calls = async_mock_service(hass, "vacuum", SERVICE_SET_FAN_SPEED) + turn_on_calls = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) + turn_off_calls = async_mock_service(hass, DOMAIN, SERVICE_TURN_OFF) + start_calls = async_mock_service(hass, DOMAIN, SERVICE_START) + pause_calls = async_mock_service(hass, DOMAIN, SERVICE_PAUSE) + stop_calls = async_mock_service(hass, DOMAIN, SERVICE_STOP) + return_calls = async_mock_service(hass, DOMAIN, SERVICE_RETURN_TO_BASE) + fan_speed_calls = async_mock_service(hass, DOMAIN, SERVICE_SET_FAN_SPEED) # These calls should do nothing as entities already in desired state await async_reproduce_state( diff --git a/tests/components/water_heater/test_device_action.py b/tests/components/water_heater/test_device_action.py index 82179c63237c43..a2bb000dd36869 100644 --- a/tests/components/water_heater/test_device_action.py +++ b/tests/components/water_heater/test_device_action.py @@ -147,8 +147,8 @@ async def test_action( }, ) - turn_off_calls = async_mock_service(hass, "water_heater", "turn_off") - turn_on_calls = async_mock_service(hass, "water_heater", "turn_on") + turn_off_calls = async_mock_service(hass, DOMAIN, "turn_off") + turn_on_calls = async_mock_service(hass, DOMAIN, "turn_on") hass.bus.async_fire("test_event_turn_off") await hass.async_block_till_done() @@ -201,7 +201,7 @@ async def test_action_legacy( }, ) - turn_off_calls = async_mock_service(hass, "water_heater", "turn_off") + turn_off_calls = async_mock_service(hass, DOMAIN, "turn_off") hass.bus.async_fire("test_event_turn_off") await hass.async_block_till_done() diff --git a/tests/components/water_heater/test_reproduce_state.py b/tests/components/water_heater/test_reproduce_state.py index 2aa10fa004f4cc..5b9935df4536a6 100644 --- a/tests/components/water_heater/test_reproduce_state.py +++ b/tests/components/water_heater/test_reproduce_state.py @@ -6,6 +6,7 @@ ATTR_AWAY_MODE, ATTR_OPERATION_MODE, ATTR_TEMPERATURE, + DOMAIN, SERVICE_SET_AWAY_MODE, SERVICE_SET_OPERATION_MODE, SERVICE_SET_TEMPERATURE, @@ -33,11 +34,11 @@ async def test_reproducing_states( {ATTR_AWAY_MODE: True, ATTR_TEMPERATURE: 45}, ) - turn_on_calls = async_mock_service(hass, "water_heater", SERVICE_TURN_ON) - turn_off_calls = async_mock_service(hass, "water_heater", SERVICE_TURN_OFF) - set_op_calls = async_mock_service(hass, "water_heater", SERVICE_SET_OPERATION_MODE) - set_temp_calls = async_mock_service(hass, "water_heater", SERVICE_SET_TEMPERATURE) - set_away_calls = async_mock_service(hass, "water_heater", SERVICE_SET_AWAY_MODE) + turn_on_calls = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) + turn_off_calls = async_mock_service(hass, DOMAIN, SERVICE_TURN_OFF) + set_op_calls = async_mock_service(hass, DOMAIN, SERVICE_SET_OPERATION_MODE) + set_temp_calls = async_mock_service(hass, DOMAIN, SERVICE_SET_TEMPERATURE) + set_away_calls = async_mock_service(hass, DOMAIN, SERVICE_SET_AWAY_MODE) # These calls should do nothing as entities already in desired state await async_reproduce_state( diff --git a/tests/components/waze_travel_time/test_init.py b/tests/components/waze_travel_time/test_init.py index 1f0b98cd159b49..103ef2fd416dd7 100644 --- a/tests/components/waze_travel_time/test_init.py +++ b/tests/components/waze_travel_time/test_init.py @@ -57,7 +57,7 @@ async def call_service_get_travel_times( if base_coordinates is not None: params["base_coordinates"] = base_coordinates return await hass.services.async_call( - "waze_travel_time", + DOMAIN, "get_travel_times", params, blocking=True, @@ -131,7 +131,7 @@ async def test_service_get_travel_times_empty_response( """Test service get_travel_times.""" mock_update.return_value = [] response_data = await hass.services.async_call( - "waze_travel_time", + DOMAIN, "get_travel_times", { "origin": "location1", diff --git a/tests/components/xbox/test_config_flow.py b/tests/components/xbox/test_config_flow.py index cee4ac31fda064..c80c8c509fe76f 100644 --- a/tests/components/xbox/test_config_flow.py +++ b/tests/components/xbox/test_config_flow.py @@ -50,7 +50,7 @@ async def test_full_flow( """Check full flow.""" result = await hass.config_entries.flow.async_init( - "xbox", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) state = config_entry_oauth2_flow._encode_jwt( hass, @@ -134,7 +134,7 @@ async def test_discovery( """Check DHCP/SSDP discovery.""" result = await hass.config_entries.flow.async_init( - "xbox", context={"source": source}, data=service_info + DOMAIN, context={"source": source}, data=service_info ) assert result["type"] is FlowResultType.FORM diff --git a/tests/components/yardian/snapshots/test_binary_sensor.ambr b/tests/components/yardian/snapshots/test_binary_sensor.ambr index eb034d8c36ed5d..fe761ab1e84b15 100644 --- a/tests/components/yardian/snapshots/test_binary_sensor.ambr +++ b/tests/components/yardian/snapshots/test_binary_sensor.ambr @@ -151,7 +151,7 @@ 'state': 'on', }) # --- -# name: test_all_entities[binary_sensor.yardian_smart_sprinkler_zone_1_enabled-entry] +# name: test_all_entities[binary_sensor.zone_1-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, @@ -165,7 +165,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.yardian_smart_sprinkler_zone_1_enabled', + 'entity_id': 'binary_sensor.zone_1', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -173,35 +173,35 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Zone 1 enabled', + 'object_id_base': None, 'options': dict({ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Zone 1 enabled', + 'original_name': None, 'platform': 'yardian', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'zone_enabled', + 'translation_key': 'enabled', 'unique_id': 'yid123-zone_enabled_0', 'unit_of_measurement': None, }) # --- -# name: test_all_entities[binary_sensor.yardian_smart_sprinkler_zone_1_enabled-state] +# name: test_all_entities[binary_sensor.zone_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Yardian Smart Sprinkler Zone 1 enabled', + 'friendly_name': 'Zone 1', }), 'context': , - 'entity_id': 'binary_sensor.yardian_smart_sprinkler_zone_1_enabled', + 'entity_id': 'binary_sensor.zone_1', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'on', }) # --- -# name: test_all_entities[binary_sensor.yardian_smart_sprinkler_zone_2_enabled-entry] +# name: test_all_entities[binary_sensor.zone_2-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, @@ -215,7 +215,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.yardian_smart_sprinkler_zone_2_enabled', + 'entity_id': 'binary_sensor.zone_2', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -223,28 +223,28 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Zone 2 enabled', + 'object_id_base': None, 'options': dict({ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Zone 2 enabled', + 'original_name': None, 'platform': 'yardian', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'zone_enabled', + 'translation_key': 'enabled', 'unique_id': 'yid123-zone_enabled_1', 'unit_of_measurement': None, }) # --- -# name: test_all_entities[binary_sensor.yardian_smart_sprinkler_zone_2_enabled-state] +# name: test_all_entities[binary_sensor.zone_2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Yardian Smart Sprinkler Zone 2 enabled', + 'friendly_name': 'Zone 2', }), 'context': , - 'entity_id': 'binary_sensor.yardian_smart_sprinkler_zone_2_enabled', + 'entity_id': 'binary_sensor.zone_2', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/yardian/snapshots/test_switch.ambr b/tests/components/yardian/snapshots/test_switch.ambr index 740a1baf9550c2..c3199cbce79f10 100644 --- a/tests/components/yardian/snapshots/test_switch.ambr +++ b/tests/components/yardian/snapshots/test_switch.ambr @@ -1,5 +1,5 @@ # serializer version: 1 -# name: test_all_entities[switch.yardian_smart_sprinkler_zone_1-entry] +# name: test_all_entities[switch.zone_1-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, @@ -13,7 +13,7 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': None, - 'entity_id': 'switch.yardian_smart_sprinkler_zone_1', + 'entity_id': 'switch.zone_1', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -21,35 +21,35 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Zone 1', + 'object_id_base': None, 'options': dict({ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Zone 1', + 'original_name': None, 'platform': 'yardian', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'switch', + 'translation_key': None, 'unique_id': 'yid123-0', 'unit_of_measurement': None, }) # --- -# name: test_all_entities[switch.yardian_smart_sprinkler_zone_1-state] +# name: test_all_entities[switch.zone_1-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Yardian Smart Sprinkler Zone 1', + 'friendly_name': 'Zone 1', }), 'context': , - 'entity_id': 'switch.yardian_smart_sprinkler_zone_1', + 'entity_id': 'switch.zone_1', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'on', }) # --- -# name: test_all_entities[switch.yardian_smart_sprinkler_zone_2-entry] +# name: test_all_entities[switch.zone_2-entry] EntityRegistryEntrySnapshot({ 'aliases': list([ None, @@ -63,7 +63,7 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': None, - 'entity_id': 'switch.yardian_smart_sprinkler_zone_2', + 'entity_id': 'switch.zone_2', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -71,28 +71,28 @@ 'labels': set({ }), 'name': None, - 'object_id_base': 'Zone 2', + 'object_id_base': None, 'options': dict({ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'Zone 2', + 'original_name': None, 'platform': 'yardian', 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': 0, - 'translation_key': 'switch', + 'translation_key': None, 'unique_id': 'yid123-1', 'unit_of_measurement': None, }) # --- -# name: test_all_entities[switch.yardian_smart_sprinkler_zone_2-state] +# name: test_all_entities[switch.zone_2-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Yardian Smart Sprinkler Zone 2', + 'friendly_name': 'Zone 2', }), 'context': , - 'entity_id': 'switch.yardian_smart_sprinkler_zone_2', + 'entity_id': 'switch.zone_2', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/yardian/test_switch.py b/tests/components/yardian/test_switch.py index 8f022bb35cb6a0..f7c713b07c3ed5 100644 --- a/tests/components/yardian/test_switch.py +++ b/tests/components/yardian/test_switch.py @@ -42,7 +42,8 @@ async def test_turn_on_switch( """Test turning on a switch.""" await setup_integration(hass, mock_config_entry) - entity_id = "switch.yardian_smart_sprinkler_zone_1" + entity_id = "switch.zone_1" + await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, @@ -61,7 +62,8 @@ async def test_turn_off_switch( """Test turning off a switch.""" await setup_integration(hass, mock_config_entry) - entity_id = "switch.yardian_smart_sprinkler_zone_1" + entity_id = "switch.zone_1" + await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_OFF, diff --git a/tests/components/youtube/test_config_flow.py b/tests/components/youtube/test_config_flow.py index be7085c9d4278c..03bfb674e3990e 100644 --- a/tests/components/youtube/test_config_flow.py +++ b/tests/components/youtube/test_config_flow.py @@ -33,7 +33,7 @@ async def test_full_flow( ) -> None: """Check full flow.""" result = await hass.config_entries.flow.async_init( - "youtube", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) state = config_entry_oauth2_flow._encode_jwt( hass, @@ -92,7 +92,7 @@ async def test_flow_abort_without_channel( ) -> None: """Check abort flow if user has no channel.""" result = await hass.config_entries.flow.async_init( - "youtube", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) state = config_entry_oauth2_flow._encode_jwt( hass, @@ -133,7 +133,7 @@ async def test_flow_abort_without_subscriptions( ) -> None: """Check abort flow if user has no subscriptions and no own channel.""" result = await hass.config_entries.flow.async_init( - "youtube", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) state = config_entry_oauth2_flow._encode_jwt( hass, @@ -178,7 +178,7 @@ async def test_flow_without_subscriptions( ) -> None: """Check flow continues without subscriptions using own channel.""" result = await hass.config_entries.flow.async_init( - "youtube", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) state = config_entry_oauth2_flow._encode_jwt( hass, @@ -241,7 +241,7 @@ async def test_flow_http_error( ) -> None: """Check full flow.""" result = await hass.config_entries.flow.async_init( - "youtube", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) state = config_entry_oauth2_flow._encode_jwt( hass, @@ -398,7 +398,7 @@ async def test_flow_exception( ) -> None: """Check full flow.""" result = await hass.config_entries.flow.async_init( - "youtube", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) state = config_entry_oauth2_flow._encode_jwt( hass, @@ -461,7 +461,7 @@ async def test_own_channel_included( ) -> None: """Test user's own channel is included in selectable channels.""" result = await hass.config_entries.flow.async_init( - "youtube", context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": config_entries.SOURCE_USER} ) state = config_entry_oauth2_flow._encode_jwt( hass, diff --git a/tests/components/zha/test_lock.py b/tests/components/zha/test_lock.py index 1fa893d34d4c2a..a7626a83c821ec 100644 --- a/tests/components/zha/test_lock.py +++ b/tests/components/zha/test_lock.py @@ -11,6 +11,7 @@ import zigpy.zcl.foundation as zcl_f from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockState +from homeassistant.components.zha import DOMAIN from homeassistant.components.zha.helpers import ( ZHADeviceProxy, ZHAGatewayProxy, @@ -143,7 +144,7 @@ async def async_set_user_code(hass: HomeAssistant, cluster: Cluster, entity_id: with patch("zigpy.zcl.Cluster.request", return_value=[zcl_f.Status.SUCCESS]): # set lock code via service call await hass.services.async_call( - "zha", + DOMAIN, "set_lock_user_code", {"entity_id": entity_id, "code_slot": 3, "user_code": "13246579"}, blocking=True, @@ -167,7 +168,7 @@ async def async_clear_user_code(hass: HomeAssistant, cluster: Cluster, entity_id with patch("zigpy.zcl.Cluster.request", return_value=[zcl_f.Status.SUCCESS]): # set lock code via service call await hass.services.async_call( - "zha", + DOMAIN, "clear_lock_user_code", { "entity_id": entity_id, @@ -189,7 +190,7 @@ async def async_enable_user_code(hass: HomeAssistant, cluster: Cluster, entity_i with patch("zigpy.zcl.Cluster.request", return_value=[zcl_f.Status.SUCCESS]): # set lock code via service call await hass.services.async_call( - "zha", + DOMAIN, "enable_lock_user_code", { "entity_id": entity_id, @@ -214,7 +215,7 @@ async def async_disable_user_code( with patch("zigpy.zcl.Cluster.request", return_value=[zcl_f.Status.SUCCESS]): # set lock code via service call await hass.services.async_call( - "zha", + DOMAIN, "disable_lock_user_code", { "entity_id": entity_id, diff --git a/tests/components/zone/test_init.py b/tests/components/zone/test_init.py index a26f50a20ee65c..c3f34a891cb324 100644 --- a/tests/components/zone/test_init.py +++ b/tests/components/zone/test_init.py @@ -65,7 +65,7 @@ async def _storage(items=None, config=None): async def test_setup_no_zones_still_adds_home_zone(hass: HomeAssistant) -> None: """Test if no config is passed in we still get the home zone.""" assert await setup.async_setup_component(hass, zone.DOMAIN, {"zone": None}) - assert len(hass.states.async_entity_ids("zone")) == 1 + assert len(hass.states.async_entity_ids(DOMAIN)) == 1 state = hass.states.get("zone.home") assert hass.config.location_name == state.name assert hass.config.latitude == state.attributes["latitude"] @@ -84,7 +84,7 @@ async def test_setup(hass: HomeAssistant) -> None: } assert await setup.async_setup_component(hass, zone.DOMAIN, {"zone": info}) - assert len(hass.states.async_entity_ids("zone")) == 2 + assert len(hass.states.async_entity_ids(DOMAIN)) == 2 state = hass.states.get("zone.test_zone") assert info["name"] == state.name assert info["latitude"] == state.attributes["latitude"] @@ -98,7 +98,7 @@ async def test_setup_zone_skips_home_zone(hass: HomeAssistant) -> None: info = {"name": "Home", "latitude": 1.1, "longitude": -2.2} assert await setup.async_setup_component(hass, zone.DOMAIN, {"zone": info}) - assert len(hass.states.async_entity_ids("zone")) == 1 + assert len(hass.states.async_entity_ids(DOMAIN)) == 1 state = hass.states.get("zone.home") assert info["name"] == state.name @@ -107,7 +107,7 @@ async def test_setup_name_can_be_same_on_multiple_zones(hass: HomeAssistant) -> """Test that zone named Home should override hass home zone.""" info = {"name": "Test Zone", "latitude": 1.1, "longitude": -2.2} assert await setup.async_setup_component(hass, zone.DOMAIN, {"zone": [info, info]}) - assert len(hass.states.async_entity_ids("zone")) == 3 + assert len(hass.states.async_entity_ids(DOMAIN)) == 3 async def test_active_zone_skips_passive_zones(hass: HomeAssistant) -> None: @@ -811,7 +811,7 @@ async def test_state(hass: HomeAssistant) -> None: } assert await setup.async_setup_component(hass, zone.DOMAIN, {"zone": info}) - assert len(hass.states.async_entity_ids("zone")) == 2 + assert len(hass.states.async_entity_ids(DOMAIN)) == 2 state = hass.states.get("zone.test_zone") assert state.state == "0" assert state.attributes[ATTR_PERSONS] == [] diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 6fd2c9b1173071..caaf921c805493 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -2242,8 +2242,8 @@ async def test_condition_template_error_traced_not_logged( """Test template errors are added to the trace and not logged when opted in. The subscribe_condition websocket command re-evaluates a condition every - second and opts in via trace.record_template_errors(). Template variable - errors must then be recorded in the trace instead of being logged repeatedly. + second and opts in via trace.suppress_template_error_logging(). Template + variable errors are then recorded in the trace without being logged. """ caplog.set_level(logging.WARNING) config = {"condition": "template", "value_template": value_template} @@ -2251,7 +2251,7 @@ async def test_condition_template_error_traced_not_logged( config = await condition.async_validate_condition_config(hass, config) test = await condition.async_from_config(hass, config) - with expectation, trace.record_template_errors(): + with expectation, trace.suppress_template_error_logging(): test.async_check() # The template errors are recorded in the trace... @@ -2269,11 +2269,10 @@ async def test_condition_template_error_logged_without_opt_in( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, ) -> None: - """Test template errors are logged when recording is not opted in. + """Test template errors are logged when suppression is not opted in. - An active trace is not enough to suppress logging; the consumer must opt in - via trace.record_template_errors(). Without it, the error is logged as usual - and not recorded in the trace. + The error is always recorded in the trace, but unless the consumer opts in + via trace.suppress_template_error_logging() it is also logged as usual. """ caplog.set_level(logging.WARNING) config = {"condition": "template", "value_template": "{{ no_such_variable }}"} @@ -2283,10 +2282,13 @@ async def test_condition_template_error_logged_without_opt_in( assert test.async_check() is False - assert "Template variable warning: 'no_such_variable' is undefined" in caplog.text + # Recorded in the trace... condition_trace = trace.trace_get(clear=False) trace.trace_clear() - assert condition_trace[""][0].template_errors == [] + assert condition_trace[""][0].template_errors == ["'no_such_variable' is undefined"] + + # ...and also logged + assert "Template variable warning: 'no_such_variable' is undefined" in caplog.text async def test_condition_template_invalid_results(hass: HomeAssistant) -> None: diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index a905c012ea94e1..657cfb3910aeb1 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -868,7 +868,8 @@ async def test_delay_template_invalid( { "error": ( "offset should be format 'HH:MM', 'HH:MM:SS' or 'HH:MM:SS.F'" - ) + ), + "template_errors": ["'invalid_delay' is undefined"], } ], }, @@ -933,7 +934,12 @@ async def test_delay_template_complex_invalid( assert_action_trace( { "0": [{"result": {"event": "test_event", "event_data": {}}}], - "1": [{"error": "expected float for dictionary value @ data['seconds']"}], + "1": [ + { + "error": "expected float for dictionary value @ data['seconds']", + "template_errors": ["'invalid_delay' is undefined"], + } + ], }, expected_script_execution="aborted", ) @@ -2646,7 +2652,12 @@ async def test_repeat_for_each_invalid_template( assert_action_trace( { - "0": [{"error": "Repeat 'for_each' must be a list of items"}], + "0": [ + { + "error": "Repeat 'for_each' must be a list of items", + "template_errors": ["'Muhaha' is undefined"], + } + ], }, expected_script_execution="aborted", ) @@ -2715,7 +2726,12 @@ async def test_repeat_condition_warning( expected_trace[f"0/repeat/{condition}/0"] = [ {"error": "In 'numeric_state':\n " + expected_error} ] - expected_trace[f"0/repeat/{condition}/0/entity_id/0"] = [{"error": expected_error}] + expected_trace[f"0/repeat/{condition}/0/entity_id/0"] = [ + { + "error": expected_error, + "template_errors": ["'unassigned_variable' is undefined"], + } + ] assert_action_trace(expected_trace)