diff --git a/homeassistant/components/airos/__init__.py b/homeassistant/components/airos/__init__.py index 3d8ecf4a5e0711..9eea047f9b7ef5 100644 --- a/homeassistant/components/airos/__init__.py +++ b/homeassistant/components/airos/__init__.py @@ -4,10 +4,18 @@ from airos.airos8 import AirOS8 -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession +from .const import DEFAULT_SSL, DEFAULT_VERIFY_SSL, SECTION_ADVANCED_SETTINGS from .coordinator import AirOSConfigEntry, AirOSDataUpdateCoordinator _PLATFORMS: list[Platform] = [ @@ -21,13 +29,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> boo # By default airOS 8 comes with self-signed SSL certificates, # with no option in the web UI to change or upload a custom certificate. - session = async_get_clientsession(hass, verify_ssl=False) + session = async_get_clientsession( + hass, verify_ssl=entry.data[SECTION_ADVANCED_SETTINGS][CONF_VERIFY_SSL] + ) airos_device = AirOS8( host=entry.data[CONF_HOST], username=entry.data[CONF_USERNAME], password=entry.data[CONF_PASSWORD], session=session, + use_ssl=entry.data[SECTION_ADVANCED_SETTINGS][CONF_SSL], ) coordinator = AirOSDataUpdateCoordinator(hass, entry, airos_device) @@ -40,6 +51,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> boo return True +async def async_migrate_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool: + """Migrate old config entry.""" + + if entry.version > 1: + # This means the user has downgraded from a future version + return False + + if entry.version == 1 and entry.minor_version == 1: + new_data = {**entry.data} + advanced_data = { + CONF_SSL: DEFAULT_SSL, + CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL, + } + new_data[SECTION_ADVANCED_SETTINGS] = advanced_data + + hass.config_entries.async_update_entry( + entry, + data=new_data, + minor_version=2, + ) + + return True + + async def async_unload_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS) diff --git a/homeassistant/components/airos/config_flow.py b/homeassistant/components/airos/config_flow.py index e66878221fea68..f0e4b48a8cc067 100644 --- a/homeassistant/components/airos/config_flow.py +++ b/homeassistant/components/airos/config_flow.py @@ -15,10 +15,17 @@ import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +from homeassistant.data_entry_flow import section from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import DOMAIN +from .const import DEFAULT_SSL, DEFAULT_VERIFY_SSL, DOMAIN, SECTION_ADVANCED_SETTINGS from .coordinator import AirOS8 _LOGGER = logging.getLogger(__name__) @@ -28,6 +35,15 @@ vol.Required(CONF_HOST): str, vol.Required(CONF_USERNAME, default="ubnt"): str, vol.Required(CONF_PASSWORD): str, + vol.Required(SECTION_ADVANCED_SETTINGS): section( + vol.Schema( + { + vol.Required(CONF_SSL, default=DEFAULT_SSL): bool, + vol.Required(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): bool, + } + ), + {"collapsed": True}, + ), } ) @@ -36,6 +52,7 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Ubiquiti airOS.""" VERSION = 1 + MINOR_VERSION = 2 async def async_step_user( self, @@ -46,13 +63,17 @@ async def async_step_user( if user_input is not None: # By default airOS 8 comes with self-signed SSL certificates, # with no option in the web UI to change or upload a custom certificate. - session = async_get_clientsession(self.hass, verify_ssl=False) + session = async_get_clientsession( + self.hass, + verify_ssl=user_input[SECTION_ADVANCED_SETTINGS][CONF_VERIFY_SSL], + ) airos_device = AirOS8( host=user_input[CONF_HOST], username=user_input[CONF_USERNAME], password=user_input[CONF_PASSWORD], session=session, + use_ssl=user_input[SECTION_ADVANCED_SETTINGS][CONF_SSL], ) try: await airos_device.login() diff --git a/homeassistant/components/airos/const.py b/homeassistant/components/airos/const.py index f4be2594613c6e..29a5f6a9e55b2a 100644 --- a/homeassistant/components/airos/const.py +++ b/homeassistant/components/airos/const.py @@ -7,3 +7,8 @@ SCAN_INTERVAL = timedelta(minutes=1) MANUFACTURER = "Ubiquiti" + +DEFAULT_VERIFY_SSL = False +DEFAULT_SSL = True + +SECTION_ADVANCED_SETTINGS = "advanced_settings" diff --git a/homeassistant/components/airos/entity.py b/homeassistant/components/airos/entity.py index e54962110fc155..0b1245694c1ec1 100644 --- a/homeassistant/components/airos/entity.py +++ b/homeassistant/components/airos/entity.py @@ -2,11 +2,11 @@ from __future__ import annotations -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, CONF_SSL from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN, MANUFACTURER +from .const import DOMAIN, MANUFACTURER, SECTION_ADVANCED_SETTINGS from .coordinator import AirOSDataUpdateCoordinator @@ -20,9 +20,14 @@ def __init__(self, coordinator: AirOSDataUpdateCoordinator) -> None: super().__init__(coordinator) airos_data = self.coordinator.data + url_schema = ( + "https" + if coordinator.config_entry.data[SECTION_ADVANCED_SETTINGS][CONF_SSL] + else "http" + ) configuration_url: str | None = ( - f"https://{coordinator.config_entry.data[CONF_HOST]}" + f"{url_schema}://{coordinator.config_entry.data[CONF_HOST]}" ) self._attr_device_info = DeviceInfo( diff --git a/homeassistant/components/airos/strings.json b/homeassistant/components/airos/strings.json index 53681292f50ab3..a6e83aae8692b4 100644 --- a/homeassistant/components/airos/strings.json +++ b/homeassistant/components/airos/strings.json @@ -12,6 +12,18 @@ "host": "IP address or hostname of the airOS device", "username": "Administrator username for the airOS device, normally 'ubnt'", "password": "Password configured through the UISP app or web interface" + }, + "sections": { + "advanced_settings": { + "data": { + "ssl": "Use HTTPS", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" + }, + "data_description": { + "ssl": "Whether the connection should be encrypted (required for most devices)", + "verify_ssl": "Whether the certificate should be verified when using HTTPS. This should be off for self-signed certificates" + } + } } } }, diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index 8af0c9157b5ba2..764a036bb35c21 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -1308,7 +1308,9 @@ async def tts_input_stream_generator() -> AsyncGenerator[str]: # instead of a full response. all_targets_in_satellite_area = ( self._get_all_targets_in_satellite_area( - conversation_result.response, self._device_id + conversation_result.response, + self._satellite_id, + self._device_id, ) ) @@ -1337,39 +1339,62 @@ async def tts_input_stream_generator() -> AsyncGenerator[str]: return (speech, all_targets_in_satellite_area) def _get_all_targets_in_satellite_area( - self, intent_response: intent.IntentResponse, device_id: str | None + self, + intent_response: intent.IntentResponse, + satellite_id: str | None, + device_id: str | None, ) -> bool: """Return true if all targeted entities were in the same area as the device.""" if ( - (intent_response.response_type != intent.IntentResponseType.ACTION_DONE) - or (not intent_response.matched_states) - or (not device_id) + intent_response.response_type != intent.IntentResponseType.ACTION_DONE + or not intent_response.matched_states ): return False + entity_registry = er.async_get(self.hass) device_registry = dr.async_get(self.hass) - if (not (device := device_registry.async_get(device_id))) or ( - not device.area_id + area_id: str | None = None + + if ( + satellite_id is not None + and (target_entity_entry := entity_registry.async_get(satellite_id)) + is not None ): - return False + area_id = target_entity_entry.area_id + device_id = target_entity_entry.device_id + + if area_id is None: + if device_id is None: + return False + + device_entry = device_registry.async_get(device_id) + if device_entry is None: + return False + + area_id = device_entry.area_id + if area_id is None: + return False - entity_registry = er.async_get(self.hass) for state in intent_response.matched_states: - entity = entity_registry.async_get(state.entity_id) - if not entity: + target_entity_entry = entity_registry.async_get(state.entity_id) + if target_entity_entry is None: return False - if (entity_area_id := entity.area_id) is None: - if (entity.device_id is None) or ( - (entity_device := device_registry.async_get(entity.device_id)) - is None - ): + target_area_id = target_entity_entry.area_id + if target_area_id is None: + if target_entity_entry.device_id is None: + return False + + target_device_entry = device_registry.async_get( + target_entity_entry.device_id + ) + if target_device_entry is None: return False - entity_area_id = entity_device.area_id + target_area_id = target_device_entry.area_id - if entity_area_id != device.area_id: + if target_area_id != area_id: return False return True diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 618711c5354a56..58a923e2dbeb0d 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20250925.1"] + "requirements": ["home-assistant-frontend==20250926.0"] } diff --git a/homeassistant/components/lamarzocco/__init__.py b/homeassistant/components/lamarzocco/__init__.py index 96d4f4c61ac4cd..2e2c8133305880 100644 --- a/homeassistant/components/lamarzocco/__init__.py +++ b/homeassistant/components/lamarzocco/__init__.py @@ -142,7 +142,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) - ) coordinators = LaMarzoccoRuntimeData( - LaMarzoccoConfigUpdateCoordinator(hass, entry, device), + LaMarzoccoConfigUpdateCoordinator(hass, entry, device, cloud_client), LaMarzoccoSettingsUpdateCoordinator(hass, entry, device), LaMarzoccoScheduleUpdateCoordinator(hass, entry, device), LaMarzoccoStatisticsUpdateCoordinator(hass, entry, device), diff --git a/homeassistant/components/lamarzocco/coordinator.py b/homeassistant/components/lamarzocco/coordinator.py index b6379f237ae46f..b5fa0ed9028f54 100644 --- a/homeassistant/components/lamarzocco/coordinator.py +++ b/homeassistant/components/lamarzocco/coordinator.py @@ -8,7 +8,7 @@ import logging from typing import Any -from pylamarzocco import LaMarzoccoMachine +from pylamarzocco import LaMarzoccoCloudClient, LaMarzoccoMachine from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful from homeassistant.config_entries import ConfigEntry @@ -19,7 +19,7 @@ from .const import DOMAIN -SCAN_INTERVAL = timedelta(seconds=15) +SCAN_INTERVAL = timedelta(seconds=60) SETTINGS_UPDATE_INTERVAL = timedelta(hours=8) SCHEDULE_UPDATE_INTERVAL = timedelta(minutes=30) STATISTICS_UPDATE_INTERVAL = timedelta(minutes=15) @@ -51,6 +51,7 @@ def __init__( hass: HomeAssistant, entry: LaMarzoccoConfigEntry, device: LaMarzoccoMachine, + cloud_client: LaMarzoccoCloudClient | None = None, ) -> None: """Initialize coordinator.""" super().__init__( @@ -61,6 +62,7 @@ def __init__( update_interval=self._default_update_interval, ) self.device = device + self.cloud_client = cloud_client async def _async_update_data(self) -> None: """Do the data update.""" @@ -85,11 +87,17 @@ async def _internal_async_update_data(self) -> None: class LaMarzoccoConfigUpdateCoordinator(LaMarzoccoUpdateCoordinator): """Class to handle fetching data from the La Marzocco API centrally.""" + cloud_client: LaMarzoccoCloudClient + async def _internal_async_update_data(self) -> None: """Fetch data from API endpoint.""" + # ensure token stays valid; does nothing if token is still valid + await self.cloud_client.async_get_access_token() + if self.device.websocket.connected: return + await self.device.get_dashboard() _LOGGER.debug("Current status: %s", self.device.dashboard.to_dict()) diff --git a/homeassistant/components/letpot/number.py b/homeassistant/components/letpot/number.py index a5b9c3df68c123..2061b419ddb56d 100644 --- a/homeassistant/components/letpot/number.py +++ b/homeassistant/components/letpot/number.py @@ -12,7 +12,7 @@ NumberEntityDescription, NumberMode, ) -from homeassistant.const import PRECISION_WHOLE, EntityCategory +from homeassistant.const import PRECISION_WHOLE, EntityCategory, UnitOfTime from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback @@ -72,6 +72,7 @@ class LetPotNumberEntityDescription(LetPotEntityDescription, NumberEntityDescrip LetPotNumberEntityDescription( key="plant_days", translation_key="plant_days", + native_unit_of_measurement=UnitOfTime.DAYS, value_fn=lambda coordinator: coordinator.data.plant_days, set_value_fn=( lambda device_client, serial, value: device_client.set_plant_days( diff --git a/homeassistant/components/letpot/strings.json b/homeassistant/components/letpot/strings.json index 4c46e1ddbb1621..3af8c7e3db672d 100644 --- a/homeassistant/components/letpot/strings.json +++ b/homeassistant/components/letpot/strings.json @@ -54,8 +54,7 @@ "name": "Light brightness" }, "plant_days": { - "name": "Plants age", - "unit_of_measurement": "days" + "name": "Plants age" } }, "select": { diff --git a/homeassistant/components/mealie/config_flow.py b/homeassistant/components/mealie/config_flow.py index 2addd23284ee45..25e46ec6262e4a 100644 --- a/homeassistant/components/mealie/config_flow.py +++ b/homeassistant/components/mealie/config_flow.py @@ -7,8 +7,9 @@ import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_API_TOKEN, CONF_HOST, CONF_VERIFY_SSL +from homeassistant.const import CONF_API_TOKEN, CONF_HOST, CONF_PORT, CONF_VERIFY_SSL from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.service_info.hassio import HassioServiceInfo from .const import DOMAIN, LOGGER, MIN_REQUIRED_MEALIE_VERSION from .utils import create_version @@ -25,13 +26,21 @@ vol.Required(CONF_API_TOKEN): str, } ) +DISCOVERY_SCHEMA = vol.Schema( + { + vol.Required(CONF_API_TOKEN): str, + } +) class MealieConfigFlow(ConfigFlow, domain=DOMAIN): """Mealie config flow.""" + VERSION = 1 + host: str | None = None verify_ssl: bool = True + _hassio_discovery: dict[str, Any] | None = None async def check_connection( self, api_token: str @@ -143,3 +152,59 @@ async def async_step_reconfigure( data_schema=USER_SCHEMA, errors=errors, ) + + async def async_step_hassio( + self, discovery_info: HassioServiceInfo + ) -> ConfigFlowResult: + """Prepare configuration for a Mealie add-on. + + This flow is triggered by the discovery component. + """ + await self._async_handle_discovery_without_unique_id() + + self._hassio_discovery = discovery_info.config + + return await self.async_step_hassio_confirm() + + async def async_step_hassio_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Confirm Supervisor discovery and prompt for API token.""" + if user_input is None: + return await self._show_hassio_form() + + assert self._hassio_discovery + + self.host = ( + f"{self._hassio_discovery[CONF_HOST]}:{self._hassio_discovery[CONF_PORT]}" + ) + self.verify_ssl = True + + errors, user_id = await self.check_connection( + user_input[CONF_API_TOKEN], + ) + + if not errors: + await self.async_set_unique_id(user_id) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title="Mealie", + data={ + CONF_HOST: self.host, + CONF_API_TOKEN: user_input[CONF_API_TOKEN], + CONF_VERIFY_SSL: self.verify_ssl, + }, + ) + return await self._show_hassio_form(errors) + + async def _show_hassio_form( + self, errors: dict[str, str] | None = None + ) -> ConfigFlowResult: + """Show the Hass.io confirmation form to the user.""" + assert self._hassio_discovery + return self.async_show_form( + step_id="hassio_confirm", + data_schema=DISCOVERY_SCHEMA, + description_placeholders={"addon": self._hassio_discovery["addon"]}, + errors=errors or {}, + ) diff --git a/homeassistant/components/mealie/quality_scale.yaml b/homeassistant/components/mealie/quality_scale.yaml index 738c5b99d911f7..93fb3ae74a0208 100644 --- a/homeassistant/components/mealie/quality_scale.yaml +++ b/homeassistant/components/mealie/quality_scale.yaml @@ -39,8 +39,14 @@ rules: # Gold devices: done diagnostics: done - discovery-update-info: todo - discovery: todo + discovery-update-info: + status: exempt + comment: | + This integration will only discover a Mealie addon that is local, not on the network. + discovery: + status: done + comment: | + The integration will discover a Mealie addon posting a discovery message. docs-data-update: done docs-examples: done docs-known-limitations: todo diff --git a/homeassistant/components/mealie/strings.json b/homeassistant/components/mealie/strings.json index 5533631f755636..8e51da6d7d11aa 100644 --- a/homeassistant/components/mealie/strings.json +++ b/homeassistant/components/mealie/strings.json @@ -39,6 +39,16 @@ "api_token": "[%key:component::mealie::common::data_description_api_token%]", "verify_ssl": "[%key:component::mealie::common::data_description_verify_ssl%]" } + }, + "hassio_confirm": { + "title": "Mealie via Home Assistant add-on", + "description": "Do you want to configure Home Assistant to connect to the Mealie instance provided by the add-on: {addon}?", + "data": { + "api_token": "[%key:common::config_flow::data::api_token%]" + }, + "data_description": { + "api_token": "[%key:component::mealie::common::data_description_api_token%]" + } } }, "error": { @@ -50,6 +60,7 @@ }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]", "wrong_account": "You have to use the same account that was used to configure the integration." diff --git a/homeassistant/components/motioneye/services.yaml b/homeassistant/components/motioneye/services.yaml index c5a11db8a6f782..483a92635e7f6e 100644 --- a/homeassistant/components/motioneye/services.yaml +++ b/homeassistant/components/motioneye/services.yaml @@ -1,8 +1,7 @@ set_text_overlay: target: - device: - integration: motioneye entity: + domain: camera integration: motioneye fields: left_text: @@ -48,9 +47,8 @@ set_text_overlay: action: target: - device: - integration: motioneye entity: + domain: camera integration: motioneye fields: action: @@ -88,7 +86,6 @@ action: snapshot: target: - device: - integration: motioneye entity: + domain: camera integration: motioneye diff --git a/homeassistant/components/nina/strings.json b/homeassistant/components/nina/strings.json index 98ea88d8798c1f..99acc636bd6f91 100644 --- a/homeassistant/components/nina/strings.json +++ b/homeassistant/components/nina/strings.json @@ -11,7 +11,7 @@ "_r_to_u": "City/county (R-U)", "_v_to_z": "City/county (V-Z)", "slots": "Maximum warnings per city/county", - "headline_filter": "Blacklist regex to filter warning headlines" + "headline_filter": "Headline blocklist" } } }, @@ -34,7 +34,7 @@ "_v_to_z": "[%key:component::nina::config::step::user::data::_v_to_z%]", "slots": "[%key:component::nina::config::step::user::data::slots%]", "headline_filter": "[%key:component::nina::config::step::user::data::headline_filter%]", - "area_filter": "Whitelist regex to filter warnings based on affected areas" + "area_filter": "Affected area filter" } } }, diff --git a/homeassistant/components/nmap_tracker/const.py b/homeassistant/components/nmap_tracker/const.py index 617f84e8acaf33..a46cbf46443dc8 100644 --- a/homeassistant/components/nmap_tracker/const.py +++ b/homeassistant/components/nmap_tracker/const.py @@ -13,6 +13,6 @@ # Interval in minutes to exclude devices from a scan while they are home CONF_HOME_INTERVAL: Final = "home_interval" CONF_OPTIONS: Final = "scan_options" -DEFAULT_OPTIONS: Final = "-F -T4 --min-rate 10 --host-timeout 5s" +DEFAULT_OPTIONS: Final = "-n -sn -PR -T4 --min-rate 10 --host-timeout 5s" TRACKER_SCAN_INTERVAL: Final = 120 diff --git a/homeassistant/components/portainer/__init__.py b/homeassistant/components/portainer/__init__.py index 602302a7c3a4d8..b945e60b545cab 100644 --- a/homeassistant/components/portainer/__init__.py +++ b/homeassistant/components/portainer/__init__.py @@ -5,7 +5,7 @@ from pyportainer import Portainer from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_API_KEY, CONF_HOST, Platform +from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_VERIFY_SSL, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_create_clientsession @@ -19,11 +19,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: PortainerConfigEntry) -> bool: """Set up Portainer from a config entry.""" - session = async_create_clientsession(hass) client = Portainer( api_url=entry.data[CONF_HOST], api_key=entry.data[CONF_API_KEY], - session=session, + session=async_create_clientsession( + hass=hass, verify_ssl=entry.data[CONF_VERIFY_SSL] + ), ) coordinator = PortainerCoordinator(hass, entry, client) diff --git a/homeassistant/components/portainer/config_flow.py b/homeassistant/components/portainer/config_flow.py index 9cf9598cc9560e..2fc4f3a722a2a7 100644 --- a/homeassistant/components/portainer/config_flow.py +++ b/homeassistant/components/portainer/config_flow.py @@ -14,7 +14,7 @@ import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_API_KEY, CONF_HOST +from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -26,6 +26,7 @@ { vol.Required(CONF_HOST): str, vol.Required(CONF_API_KEY): str, + vol.Optional(CONF_VERIFY_SSL, default=True): bool, } ) @@ -36,7 +37,7 @@ async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None: client = Portainer( api_url=data[CONF_HOST], api_key=data[CONF_API_KEY], - session=async_get_clientsession(hass), + session=async_get_clientsession(hass=hass, verify_ssl=data[CONF_VERIFY_SSL]), ) try: await client.get_endpoints() diff --git a/homeassistant/components/portainer/strings.json b/homeassistant/components/portainer/strings.json index 89530efc2129d9..acdd0d362a3bb2 100644 --- a/homeassistant/components/portainer/strings.json +++ b/homeassistant/components/portainer/strings.json @@ -4,11 +4,13 @@ "user": { "data": { "host": "[%key:common::config_flow::data::host%]", - "api_key": "[%key:common::config_flow::data::api_key%]" + "api_key": "[%key:common::config_flow::data::api_key%]", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" }, "data_description": { "host": "The host/URL, including the port, of your Portainer instance", - "api_key": "The API key for authenticating with Portainer" + "api_key": "The API key for authenticating with Portainer", + "verify_ssl": "Whether to verify SSL certificates. Disable only if you have a self-signed certificate" }, "description": "You can create an API key in the Portainer UI. Go to **My account > API keys** and select **Add API key**" } diff --git a/homeassistant/components/thread/config_flow.py b/homeassistant/components/thread/config_flow.py index bf202a50c34798..42caf5d9e32cd6 100644 --- a/homeassistant/components/thread/config_flow.py +++ b/homeassistant/components/thread/config_flow.py @@ -5,7 +5,11 @@ from typing import Any from homeassistant.components import onboarding -from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.config_entries import ( + DEFAULT_DISCOVERY_UNIQUE_ID, + ConfigFlow, + ConfigFlowResult, +) from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo from .const import DOMAIN @@ -18,14 +22,18 @@ class ThreadConfigFlow(ConfigFlow, domain=DOMAIN): async def async_step_import(self, import_data: None) -> ConfigFlowResult: """Set up by import from async_setup.""" - await self._async_handle_discovery_without_unique_id() + await self.async_set_unique_id( + DEFAULT_DISCOVERY_UNIQUE_ID, raise_on_progress=False + ) return self.async_create_entry(title="Thread", data={}) async def async_step_user( self, user_input: dict[str, str] | None = None ) -> ConfigFlowResult: """Set up by import from async_setup.""" - await self._async_handle_discovery_without_unique_id() + await self.async_set_unique_id( + DEFAULT_DISCOVERY_UNIQUE_ID, raise_on_progress=False + ) return self.async_create_entry(title="Thread", data={}) async def async_step_zeroconf( diff --git a/homeassistant/components/thread/manifest.json b/homeassistant/components/thread/manifest.json index 868ced022b8b9d..22d55f57d48d6c 100644 --- a/homeassistant/components/thread/manifest.json +++ b/homeassistant/components/thread/manifest.json @@ -8,5 +8,6 @@ "integration_type": "service", "iot_class": "local_polling", "requirements": ["python-otbr-api==2.7.0", "pyroute2==0.7.5"], + "single_config_entry": true, "zeroconf": ["_meshcop._udp.local."] } diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index 0c28faac59f896..5eeb524bc24568 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -141,7 +141,9 @@ def extra_state_attributes(self) -> dict[str, Any]: attr["active_time"] = self.device.state.active_time if hasattr(self.device.state, "display_status"): - attr["display_status"] = self.device.state.display_status.value + attr["display_status"] = getattr( + self.device.state.display_status, "value", None + ) if hasattr(self.device.state, "child_lock"): attr["child_lock"] = self.device.state.child_lock diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index e260b37afe610e..2ce0e314afb5c5 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -6807,7 +6807,8 @@ "name": "Thread", "integration_type": "service", "config_flow": true, - "iot_class": "local_polling" + "iot_class": "local_polling", + "single_config_entry": true }, "tibber": { "name": "Tibber", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 725e5269a91cde..fccb5db82cca68 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -39,7 +39,7 @@ habluetooth==5.6.4 hass-nabucasa==1.1.2 hassil==3.2.0 home-assistant-bluetooth==1.13.1 -home-assistant-frontend==20250925.1 +home-assistant-frontend==20250926.0 home-assistant-intents==2025.9.24 httpx==0.28.1 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 0f7d337801383c..6ef7bba575e7ae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1186,7 +1186,7 @@ hole==0.9.0 holidays==0.81 # homeassistant.components.frontend -home-assistant-frontend==20250925.1 +home-assistant-frontend==20250926.0 # homeassistant.components.conversation home-assistant-intents==2025.9.24 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1e7a5f288c20f6..930ea19f4460e8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1035,7 +1035,7 @@ hole==0.9.0 holidays==0.81 # homeassistant.components.frontend -home-assistant-frontend==20250925.1 +home-assistant-frontend==20250926.0 # homeassistant.components.conversation home-assistant-intents==2025.9.24 diff --git a/tests/components/airos/conftest.py b/tests/components/airos/conftest.py index a86eb8fd39bd34..8c341a670d25ff 100644 --- a/tests/components/airos/conftest.py +++ b/tests/components/airos/conftest.py @@ -1,7 +1,7 @@ """Common fixtures for the Ubiquiti airOS tests.""" from collections.abc import Generator -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from airos.airos8 import AirOS8Data import pytest @@ -28,22 +28,26 @@ def mock_setup_entry() -> Generator[AsyncMock]: yield mock_setup_entry +@pytest.fixture +def mock_airos_class() -> Generator[MagicMock]: + """Fixture to mock the AirOS class itself.""" + with ( + patch("homeassistant.components.airos.AirOS8", autospec=True) as mock_class, + patch("homeassistant.components.airos.config_flow.AirOS8", new=mock_class), + patch("homeassistant.components.airos.coordinator.AirOS8", new=mock_class), + ): + yield mock_class + + @pytest.fixture def mock_airos_client( - request: pytest.FixtureRequest, ap_fixture: AirOS8Data + mock_airos_class: MagicMock, ap_fixture: AirOS8Data ) -> Generator[AsyncMock]: """Fixture to mock the AirOS API client.""" - with ( - patch( - "homeassistant.components.airos.config_flow.AirOS8", autospec=True - ) as mock_airos, - patch("homeassistant.components.airos.coordinator.AirOS8", new=mock_airos), - patch("homeassistant.components.airos.AirOS8", new=mock_airos), - ): - client = mock_airos.return_value - client.status.return_value = ap_fixture - client.login.return_value = True - yield client + client = mock_airos_class.return_value + client.status.return_value = ap_fixture + client.login.return_value = True + return client @pytest.fixture diff --git a/tests/components/airos/snapshots/test_diagnostics.ambr b/tests/components/airos/snapshots/test_diagnostics.ambr index f4561ec6d994f2..4e94beae4733a8 100644 --- a/tests/components/airos/snapshots/test_diagnostics.ambr +++ b/tests/components/airos/snapshots/test_diagnostics.ambr @@ -632,6 +632,10 @@ }), }), 'entry_data': dict({ + 'advanced_settings': dict({ + 'ssl': True, + 'verify_ssl': False, + }), 'host': '**REDACTED**', 'password': '**REDACTED**', 'username': 'ubnt', diff --git a/tests/components/airos/test_config_flow.py b/tests/components/airos/test_config_flow.py index 212c80dfc2bf86..a502f9f2f3bc25 100644 --- a/tests/components/airos/test_config_flow.py +++ b/tests/components/airos/test_config_flow.py @@ -10,9 +10,15 @@ ) import pytest -from homeassistant.components.airos.const import DOMAIN +from homeassistant.components.airos.const import DOMAIN, SECTION_ADVANCED_SETTINGS from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -22,6 +28,10 @@ CONF_HOST: "1.1.1.1", CONF_USERNAME: "ubnt", CONF_PASSWORD: "test-password", + SECTION_ADVANCED_SETTINGS: { + CONF_SSL: True, + CONF_VERIFY_SSL: False, + }, } @@ -33,7 +43,8 @@ async def test_form_creates_entry( ) -> None: """Test we get the form and create the appropriate entry.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + DOMAIN, + context={"source": SOURCE_USER}, ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {} diff --git a/tests/components/airos/test_init.py b/tests/components/airos/test_init.py new file mode 100644 index 00000000000000..30e2498d7d763f --- /dev/null +++ b/tests/components/airos/test_init.py @@ -0,0 +1,169 @@ +"""Test for airOS integration setup.""" + +from __future__ import annotations + +from unittest.mock import ANY, MagicMock + +from homeassistant.components.airos.const import ( + DEFAULT_SSL, + DEFAULT_VERIFY_SSL, + DOMAIN, + SECTION_ADVANCED_SETTINGS, +) +from homeassistant.config_entries import SOURCE_USER, ConfigEntryState +from homeassistant.const import ( + CONF_HOST, + CONF_PASSWORD, + CONF_SSL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +MOCK_CONFIG_V1 = { + CONF_HOST: "1.1.1.1", + CONF_USERNAME: "ubnt", + CONF_PASSWORD: "test-password", +} + +MOCK_CONFIG_PLAIN = { + CONF_HOST: "1.1.1.1", + CONF_USERNAME: "ubnt", + CONF_PASSWORD: "test-password", + SECTION_ADVANCED_SETTINGS: { + CONF_SSL: False, + CONF_VERIFY_SSL: False, + }, +} + +MOCK_CONFIG_V1_2 = { + CONF_HOST: "1.1.1.1", + CONF_USERNAME: "ubnt", + CONF_PASSWORD: "test-password", + SECTION_ADVANCED_SETTINGS: { + CONF_SSL: DEFAULT_SSL, + CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL, + }, +} + + +async def test_setup_entry_with_default_ssl( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_airos_client: MagicMock, + mock_airos_class: MagicMock, +) -> None: + """Test setting up a config entry with default SSL options.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + + mock_airos_class.assert_called_once_with( + host=mock_config_entry.data[CONF_HOST], + username=mock_config_entry.data[CONF_USERNAME], + password=mock_config_entry.data[CONF_PASSWORD], + session=ANY, + use_ssl=DEFAULT_SSL, + ) + + assert mock_config_entry.data[SECTION_ADVANCED_SETTINGS][CONF_SSL] is True + assert mock_config_entry.data[SECTION_ADVANCED_SETTINGS][CONF_VERIFY_SSL] is False + + +async def test_setup_entry_without_ssl( + hass: HomeAssistant, + mock_airos_client: MagicMock, + mock_airos_class: MagicMock, +) -> None: + """Test setting up a config entry adjusted to plain HTTP.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_CONFIG_PLAIN, + entry_id="1", + unique_id="airos_device", + version=1, + minor_version=2, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.LOADED + + mock_airos_class.assert_called_once_with( + host=entry.data[CONF_HOST], + username=entry.data[CONF_USERNAME], + password=entry.data[CONF_PASSWORD], + session=ANY, + use_ssl=False, + ) + + assert entry.data[SECTION_ADVANCED_SETTINGS][CONF_SSL] is False + assert entry.data[SECTION_ADVANCED_SETTINGS][CONF_VERIFY_SSL] is False + + +async def test_migrate_entry(hass: HomeAssistant, mock_airos_client: MagicMock) -> None: + """Test migrate entry unique id.""" + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data=MOCK_CONFIG_V1, + entry_id="1", + unique_id="airos_device", + version=1, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.LOADED + assert entry.version == 1 + assert entry.minor_version == 2 + assert entry.data == MOCK_CONFIG_V1_2 + + +async def test_migrate_future_return( + hass: HomeAssistant, + mock_airos_client: MagicMock, +) -> None: + """Test migrate entry unique id.""" + entry = MockConfigEntry( + domain=DOMAIN, + source=SOURCE_USER, + data=MOCK_CONFIG_V1_2, + entry_id="1", + unique_id="airos_device", + version=2, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.MIGRATION_ERROR + + +async def test_load_unload_entry( + hass: HomeAssistant, + mock_airos_client: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test setup and unload config entry.""" + mock_config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + + assert await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/assist_pipeline/test_pipeline.py b/tests/components/assist_pipeline/test_pipeline.py index fe82f693fde1a6..fc2d6d18a6a60a 100644 --- a/tests/components/assist_pipeline/test_pipeline.py +++ b/tests/components/assist_pipeline/test_pipeline.py @@ -1797,6 +1797,7 @@ async def stream_llm_response(): assert process_events(events) == snapshot +@pytest.mark.parametrize(("use_satellite_entity"), [True, False]) async def test_acknowledge( hass: HomeAssistant, init_components, @@ -1805,6 +1806,7 @@ async def test_acknowledge( entity_registry: er.EntityRegistry, area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, + use_satellite_entity: bool, ) -> None: """Test that acknowledge sound is played when targets are in the same area.""" area_1 = area_registry.async_get_or_create("area_1") @@ -1819,12 +1821,16 @@ async def test_acknowledge( entry = MockConfigEntry() entry.add_to_hass(hass) - satellite = device_registry.async_get_or_create( + + satellite = entity_registry.async_get_or_create("assist_satellite", "test", "1234") + entity_registry.async_update_entity(satellite.entity_id, area_id=area_1.id) + + satellite_device = device_registry.async_get_or_create( config_entry_id=entry.entry_id, connections=set(), identifiers={("demo", "id-1234")}, ) - device_registry.async_update_device(satellite.id, area_id=area_1.id) + device_registry.async_update_device(satellite_device.id, area_id=area_1.id) events: list[assist_pipeline.PipelineEvent] = [] turn_on = async_mock_service(hass, "light", "turn_on") @@ -1837,7 +1843,8 @@ async def _run(text: str) -> None: pipeline_input = assist_pipeline.pipeline.PipelineInput( intent_input=text, session=mock_chat_session, - device_id=satellite.id, + satellite_id=satellite.entity_id if use_satellite_entity else None, + device_id=satellite_device.id if not use_satellite_entity else None, run=assist_pipeline.pipeline.PipelineRun( hass, context=Context(), @@ -1889,7 +1896,8 @@ def _reset() -> None: ) # 3. Remove satellite device area - device_registry.async_update_device(satellite.id, area_id=None) + entity_registry.async_update_entity(satellite.entity_id, area_id=None) + device_registry.async_update_device(satellite_device.id, area_id=None) _reset() await _run("turn on light 1") @@ -1900,7 +1908,8 @@ def _reset() -> None: assert len(turn_on) == 1 # Restore - device_registry.async_update_device(satellite.id, area_id=area_1.id) + entity_registry.async_update_entity(satellite.entity_id, area_id=area_1.id) + device_registry.async_update_device(satellite_device.id, area_id=area_1.id) # 4. Check device area instead of entity area light_device = device_registry.async_get_or_create( diff --git a/tests/components/letpot/snapshots/test_number.ambr b/tests/components/letpot/snapshots/test_number.ambr index 50f6cf64312e9d..4784cfa695a442 100644 --- a/tests/components/letpot/snapshots/test_number.ambr +++ b/tests/components/letpot/snapshots/test_number.ambr @@ -93,7 +93,7 @@ 'supported_features': 0, 'translation_key': 'plant_days', 'unique_id': 'a1b2c3d4e5f6a1b2c3d4e5f6_LPH63ABCD_plant_days', - 'unit_of_measurement': 'days', + 'unit_of_measurement': , }) # --- # name: test_all_entities[number.garden_plants_age-state] @@ -104,7 +104,7 @@ 'min': 0.0, 'mode': , 'step': 1, - 'unit_of_measurement': 'days', + 'unit_of_measurement': , }), 'context': , 'entity_id': 'number.garden_plants_age', diff --git a/tests/components/mealie/test_config_flow.py b/tests/components/mealie/test_config_flow.py index 628f0290f43e5f..f86818a933f0d6 100644 --- a/tests/components/mealie/test_config_flow.py +++ b/tests/components/mealie/test_config_flow.py @@ -6,10 +6,11 @@ import pytest from homeassistant.components.mealie.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_HASSIO, SOURCE_IGNORE, SOURCE_USER from homeassistant.const import CONF_API_TOKEN, CONF_HOST, CONF_VERIFY_SSL from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers.service_info.hassio import HassioServiceInfo from . import setup_integration @@ -361,3 +362,137 @@ async def test_reconfigure_flow_exceptions( ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reconfigure_successful" + + +async def test_hassio_success( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test successful Supervisor flow.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=HassioServiceInfo( + config={"addon": "Mealie", "host": "http://test", "port": 9090}, + name="mealie", + slug="mealie", + uuid="1234", + ), + context={"source": SOURCE_HASSIO}, + ) + + assert result.get("type") is FlowResultType.FORM + assert result.get("step_id") == "hassio_confirm" + assert result.get("description_placeholders") == {"addon": "Mealie"} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_API_TOKEN: "token"} + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "Mealie" + assert result["data"] == { + CONF_HOST: "http://test:9090", + CONF_API_TOKEN: "token", + CONF_VERIFY_SSL: True, + } + assert result["result"].unique_id == "bf1c62fe-4941-4332-9886-e54e88dbdba0" + + +async def test_hassio_already_configured( + hass: HomeAssistant, mock_config_entry: MockConfigEntry +) -> None: + """Test we only allow a single config flow.""" + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=HassioServiceInfo( + config={ + "addon": "Mealie", + "host": "mock-mealie", + "port": "9090", + }, + name="Mealie", + slug="mealie", + uuid="1234", + ), + context={"source": SOURCE_HASSIO}, + ) + assert result + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_hassio_ignored(hass: HomeAssistant) -> None: + """Test the supervisor discovered instance can be ignored.""" + MockConfigEntry(domain=DOMAIN, source=SOURCE_IGNORE).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=HassioServiceInfo( + config={ + "addon": "Mealie", + "host": "mock-mealie", + "port": "9090", + }, + name="Mealie", + slug="mealie", + uuid="1234", + ), + context={"source": SOURCE_HASSIO}, + ) + assert result + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.parametrize( + ("exception", "error"), + [ + (MealieConnectionError, "cannot_connect"), + (MealieAuthenticationError, "invalid_auth"), + (Exception, "unknown"), + ], +) +async def test_hassio_connection_error( + hass: HomeAssistant, + mock_mealie_client: AsyncMock, + mock_setup_entry: AsyncMock, + exception: Exception, + error: str, +) -> None: + """Test flow errors.""" + mock_mealie_client.get_user_info.side_effect = exception + + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=HassioServiceInfo( + config={"addon": "Mealie", "host": "http://test", "port": 9090}, + name="mealie", + slug="mealie", + uuid="1234", + ), + context={"source": SOURCE_HASSIO}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "hassio_confirm" + assert result["description_placeholders"] == {"addon": "Mealie"} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_API_TOKEN: "token"} + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} + + mock_mealie_client.get_user_info.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_API_TOKEN: "token"} + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY diff --git a/tests/components/nmap_tracker/test_config_flow.py b/tests/components/nmap_tracker/test_config_flow.py index 5c0548c4158e30..3fa366216fd65e 100644 --- a/tests/components/nmap_tracker/test_config_flow.py +++ b/tests/components/nmap_tracker/test_config_flow.py @@ -213,7 +213,7 @@ async def test_options_flow(hass: HomeAssistant) -> None: CONF_HOSTS: "192.168.1.0/24", CONF_CONSIDER_HOME: 180, CONF_SCAN_INTERVAL: 120, - CONF_OPTIONS: "-F -T4 --min-rate 10 --host-timeout 5s", + CONF_OPTIONS: "-n -sn -PR -T4 --min-rate 10 --host-timeout 5s", } with patch( diff --git a/tests/components/plugwise/snapshots/test_switch.ambr b/tests/components/plugwise/snapshots/test_switch.ambr new file mode 100644 index 00000000000000..e296b874210763 --- /dev/null +++ b/tests/components/plugwise/snapshots/test_switch.ambr @@ -0,0 +1,1264 @@ +# serializer version: 1 +# name: test_adam_switch_snapshot[platforms0][switch.cv_pomp_relay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.cv_pomp_relay', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Relay', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay', + 'unique_id': '78d1126fc4c743db81b61c20e88342a7-relay', + 'unit_of_measurement': None, + }) +# --- +# name: test_adam_switch_snapshot[platforms0][switch.cv_pomp_relay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'CV Pomp Relay', + }), + 'context': , + 'entity_id': 'switch.cv_pomp_relay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_adam_switch_snapshot[platforms0][switch.fibaro_hc2_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.fibaro_hc2_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Lock', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'lock', + 'unique_id': 'a28f588dc4a049a483fd03a30361ad3a-lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_adam_switch_snapshot[platforms0][switch.fibaro_hc2_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Fibaro HC2 Lock', + }), + 'context': , + 'entity_id': 'switch.fibaro_hc2_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_adam_switch_snapshot[platforms0][switch.fibaro_hc2_relay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.fibaro_hc2_relay', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Relay', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay', + 'unique_id': 'a28f588dc4a049a483fd03a30361ad3a-relay', + 'unit_of_measurement': None, + }) +# --- +# name: test_adam_switch_snapshot[platforms0][switch.fibaro_hc2_relay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'Fibaro HC2 Relay', + }), + 'context': , + 'entity_id': 'switch.fibaro_hc2_relay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_adam_switch_snapshot[platforms0][switch.nas_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.nas_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Lock', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'lock', + 'unique_id': 'cd0ddb54ef694e11ac18ed1cbce5dbbd-lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_adam_switch_snapshot[platforms0][switch.nas_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NAS Lock', + }), + 'context': , + 'entity_id': 'switch.nas_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_adam_switch_snapshot[platforms0][switch.nas_relay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.nas_relay', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Relay', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay', + 'unique_id': 'cd0ddb54ef694e11ac18ed1cbce5dbbd-relay', + 'unit_of_measurement': None, + }) +# --- +# name: test_adam_switch_snapshot[platforms0][switch.nas_relay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'NAS Relay', + }), + 'context': , + 'entity_id': 'switch.nas_relay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_adam_switch_snapshot[platforms0][switch.nvr_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.nvr_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Lock', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'lock', + 'unique_id': '02cf28bfec924855854c544690a609ef-lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_adam_switch_snapshot[platforms0][switch.nvr_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'NVR Lock', + }), + 'context': , + 'entity_id': 'switch.nvr_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_adam_switch_snapshot[platforms0][switch.nvr_relay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.nvr_relay', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Relay', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay', + 'unique_id': '02cf28bfec924855854c544690a609ef-relay', + 'unit_of_measurement': None, + }) +# --- +# name: test_adam_switch_snapshot[platforms0][switch.nvr_relay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'NVR Relay', + }), + 'context': , + 'entity_id': 'switch.nvr_relay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_adam_switch_snapshot[platforms0][switch.playstation_smart_plug_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.playstation_smart_plug_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Lock', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'lock', + 'unique_id': '21f2b542c49845e6bb416884c55778d6-lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_adam_switch_snapshot[platforms0][switch.playstation_smart_plug_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Playstation Smart Plug Lock', + }), + 'context': , + 'entity_id': 'switch.playstation_smart_plug_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_adam_switch_snapshot[platforms0][switch.playstation_smart_plug_relay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.playstation_smart_plug_relay', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Relay', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay', + 'unique_id': '21f2b542c49845e6bb416884c55778d6-relay', + 'unit_of_measurement': None, + }) +# --- +# name: test_adam_switch_snapshot[platforms0][switch.playstation_smart_plug_relay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'Playstation Smart Plug Relay', + }), + 'context': , + 'entity_id': 'switch.playstation_smart_plug_relay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_adam_switch_snapshot[platforms0][switch.test_relay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.test_relay', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Relay', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay', + 'unique_id': 'e8ef2a01ed3b4139a53bf749204fe6b4-relay', + 'unit_of_measurement': None, + }) +# --- +# name: test_adam_switch_snapshot[platforms0][switch.test_relay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'Test Relay', + }), + 'context': , + 'entity_id': 'switch.test_relay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_adam_switch_snapshot[platforms0][switch.usg_smart_plug_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.usg_smart_plug_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Lock', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'lock', + 'unique_id': '4a810418d5394b3f82727340b91ba740-lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_adam_switch_snapshot[platforms0][switch.usg_smart_plug_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'USG Smart Plug Lock', + }), + 'context': , + 'entity_id': 'switch.usg_smart_plug_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_adam_switch_snapshot[platforms0][switch.usg_smart_plug_relay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.usg_smart_plug_relay', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Relay', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay', + 'unique_id': '4a810418d5394b3f82727340b91ba740-relay', + 'unit_of_measurement': None, + }) +# --- +# name: test_adam_switch_snapshot[platforms0][switch.usg_smart_plug_relay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'USG Smart Plug Relay', + }), + 'context': , + 'entity_id': 'switch.usg_smart_plug_relay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_adam_switch_snapshot[platforms0][switch.ziggo_modem_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.ziggo_modem_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Lock', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'lock', + 'unique_id': '675416a629f343c495449970e2ca37b5-lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_adam_switch_snapshot[platforms0][switch.ziggo_modem_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Ziggo Modem Lock', + }), + 'context': , + 'entity_id': 'switch.ziggo_modem_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_adam_switch_snapshot[platforms0][switch.ziggo_modem_relay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.ziggo_modem_relay', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Relay', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay', + 'unique_id': '675416a629f343c495449970e2ca37b5-relay', + 'unit_of_measurement': None, + }) +# --- +# name: test_adam_switch_snapshot[platforms0][switch.ziggo_modem_relay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'Ziggo Modem Relay', + }), + 'context': , + 'entity_id': 'switch.ziggo_modem_relay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_stretch_switch_snapshot[platforms0][switch.boiler_1eb31_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.boiler_1eb31_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Lock', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'lock', + 'unique_id': '5871317346d045bc9f6b987ef25ee638-lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_stretch_switch_snapshot[platforms0][switch.boiler_1eb31_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Boiler (1EB31) Lock', + }), + 'context': , + 'entity_id': 'switch.boiler_1eb31_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_stretch_switch_snapshot[platforms0][switch.boiler_1eb31_relay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.boiler_1eb31_relay', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Relay', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay', + 'unique_id': '5871317346d045bc9f6b987ef25ee638-relay', + 'unit_of_measurement': None, + }) +# --- +# name: test_stretch_switch_snapshot[platforms0][switch.boiler_1eb31_relay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'Boiler (1EB31) Relay', + }), + 'context': , + 'entity_id': 'switch.boiler_1eb31_relay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_stretch_switch_snapshot[platforms0][switch.droger_52559_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.droger_52559_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Lock', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'lock', + 'unique_id': 'cfe95cf3de1948c0b8955125bf754614-lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_stretch_switch_snapshot[platforms0][switch.droger_52559_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Droger (52559) Lock', + }), + 'context': , + 'entity_id': 'switch.droger_52559_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_stretch_switch_snapshot[platforms0][switch.droger_52559_relay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.droger_52559_relay', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Relay', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay', + 'unique_id': 'cfe95cf3de1948c0b8955125bf754614-relay', + 'unit_of_measurement': None, + }) +# --- +# name: test_stretch_switch_snapshot[platforms0][switch.droger_52559_relay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'Droger (52559) Relay', + }), + 'context': , + 'entity_id': 'switch.droger_52559_relay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_stretch_switch_snapshot[platforms0][switch.koelkast_92c4a_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.koelkast_92c4a_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Lock', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'lock', + 'unique_id': 'e1c884e7dede431dadee09506ec4f859-lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_stretch_switch_snapshot[platforms0][switch.koelkast_92c4a_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Koelkast (92C4A) Lock', + }), + 'context': , + 'entity_id': 'switch.koelkast_92c4a_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_stretch_switch_snapshot[platforms0][switch.koelkast_92c4a_relay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.koelkast_92c4a_relay', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Relay', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay', + 'unique_id': 'e1c884e7dede431dadee09506ec4f859-relay', + 'unit_of_measurement': None, + }) +# --- +# name: test_stretch_switch_snapshot[platforms0][switch.koelkast_92c4a_relay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'Koelkast (92C4A) Relay', + }), + 'context': , + 'entity_id': 'switch.koelkast_92c4a_relay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_stretch_switch_snapshot[platforms0][switch.schakel_relay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.schakel_relay', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Relay', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay', + 'unique_id': 'd03738edfcc947f7b8f4573571d90d2d-relay', + 'unit_of_measurement': None, + }) +# --- +# name: test_stretch_switch_snapshot[platforms0][switch.schakel_relay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'Schakel Relay', + }), + 'context': , + 'entity_id': 'switch.schakel_relay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_stretch_switch_snapshot[platforms0][switch.stroomvreters_relay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.stroomvreters_relay', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Relay', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay', + 'unique_id': 'd950b314e9d8499f968e6db8d82ef78c-relay', + 'unit_of_measurement': None, + }) +# --- +# name: test_stretch_switch_snapshot[platforms0][switch.stroomvreters_relay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'Stroomvreters Relay', + }), + 'context': , + 'entity_id': 'switch.stroomvreters_relay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_stretch_switch_snapshot[platforms0][switch.vaatwasser_2a1ab_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.vaatwasser_2a1ab_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Lock', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'lock', + 'unique_id': 'aac7b735042c4832ac9ff33aae4f453b-lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_stretch_switch_snapshot[platforms0][switch.vaatwasser_2a1ab_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Vaatwasser (2a1ab) Lock', + }), + 'context': , + 'entity_id': 'switch.vaatwasser_2a1ab_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- +# name: test_stretch_switch_snapshot[platforms0][switch.vaatwasser_2a1ab_relay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.vaatwasser_2a1ab_relay', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Relay', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay', + 'unique_id': 'aac7b735042c4832ac9ff33aae4f453b-relay', + 'unit_of_measurement': None, + }) +# --- +# name: test_stretch_switch_snapshot[platforms0][switch.vaatwasser_2a1ab_relay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'Vaatwasser (2a1ab) Relay', + }), + 'context': , + 'entity_id': 'switch.vaatwasser_2a1ab_relay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_stretch_switch_snapshot[platforms0][switch.wasmachine_52ac1_lock-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': , + 'entity_id': 'switch.wasmachine_52ac1_lock', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Lock', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'lock', + 'unique_id': '059e4d03c7a34d278add5c7a4a781d19-lock', + 'unit_of_measurement': None, + }) +# --- +# name: test_stretch_switch_snapshot[platforms0][switch.wasmachine_52ac1_lock-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Wasmachine (52AC1) Lock', + }), + 'context': , + 'entity_id': 'switch.wasmachine_52ac1_lock', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- +# name: test_stretch_switch_snapshot[platforms0][switch.wasmachine_52ac1_relay-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.wasmachine_52ac1_relay', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Relay', + 'platform': 'plugwise', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'relay', + 'unique_id': '059e4d03c7a34d278add5c7a4a781d19-relay', + 'unit_of_measurement': None, + }) +# --- +# name: test_stretch_switch_snapshot[platforms0][switch.wasmachine_52ac1_relay-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'Wasmachine (52AC1) Relay', + }), + 'context': , + 'entity_id': 'switch.wasmachine_52ac1_relay', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- diff --git a/tests/components/plugwise/test_switch.py b/tests/components/plugwise/test_switch.py index 003c47ed1f4557..f04cf92c0da1f2 100644 --- a/tests/components/plugwise/test_switch.py +++ b/tests/components/plugwise/test_switch.py @@ -4,6 +4,7 @@ from plugwise.exceptions import PlugwiseException import pytest +from syrupy.assertion import SnapshotAssertion from homeassistant.components.plugwise.const import DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN @@ -19,53 +20,20 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, snapshot_platform -async def test_adam_climate_switch_entities( - hass: HomeAssistant, mock_smile_adam: MagicMock, init_integration: MockConfigEntry -) -> None: - """Test creation of climate related switch entities.""" - state = hass.states.get("switch.cv_pomp_relay") - assert state - assert state.state == STATE_ON - - state = hass.states.get("switch.fibaro_hc2_relay") - assert state - assert state.state == STATE_ON - - -async def test_adam_climate_switch_negative_testing( - hass: HomeAssistant, mock_smile_adam: MagicMock, init_integration: MockConfigEntry +@pytest.mark.parametrize("platforms", [(SWITCH_DOMAIN,)]) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_adam_switch_snapshot( + hass: HomeAssistant, + mock_smile_adam: MagicMock, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + setup_platform: MockConfigEntry, ) -> None: - """Test exceptions of climate related switch entities.""" - mock_smile_adam.set_switch_state.side_effect = PlugwiseException - - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "switch.cv_pomp_relay"}, - blocking=True, - ) - - assert mock_smile_adam.set_switch_state.call_count == 1 - mock_smile_adam.set_switch_state.assert_called_with( - "78d1126fc4c743db81b61c20e88342a7", None, "relay", STATE_OFF - ) - - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.fibaro_hc2_relay"}, - blocking=True, - ) - - assert mock_smile_adam.set_switch_state.call_count == 2 - mock_smile_adam.set_switch_state.assert_called_with( - "a28f588dc4a049a483fd03a30361ad3a", None, "relay", STATE_ON - ) + """Test Adam switch snapshot.""" + await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) async def test_adam_climate_switch_changes( @@ -109,17 +77,50 @@ async def test_adam_climate_switch_changes( ) -async def test_stretch_switch_entities( - hass: HomeAssistant, mock_stretch: MagicMock, init_integration: MockConfigEntry +async def test_adam_climate_switch_negative_testing( + hass: HomeAssistant, mock_smile_adam: MagicMock, init_integration: MockConfigEntry +) -> None: + """Test exceptions of climate related switch entities.""" + mock_smile_adam.set_switch_state.side_effect = PlugwiseException + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.cv_pomp_relay"}, + blocking=True, + ) + + assert mock_smile_adam.set_switch_state.call_count == 1 + mock_smile_adam.set_switch_state.assert_called_with( + "78d1126fc4c743db81b61c20e88342a7", None, "relay", STATE_OFF + ) + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.fibaro_hc2_relay"}, + blocking=True, + ) + + assert mock_smile_adam.set_switch_state.call_count == 2 + mock_smile_adam.set_switch_state.assert_called_with( + "a28f588dc4a049a483fd03a30361ad3a", None, "relay", STATE_ON + ) + + +@pytest.mark.parametrize("platforms", [(SWITCH_DOMAIN,)]) +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_stretch_switch_snapshot( + hass: HomeAssistant, + mock_stretch: MagicMock, + snapshot: SnapshotAssertion, + entity_registry: er.EntityRegistry, + setup_platform: MockConfigEntry, ) -> None: - """Test creation of climate related switch entities.""" - state = hass.states.get("switch.koelkast_92c4a_relay") - assert state - assert state.state == STATE_ON - - state = hass.states.get("switch.droger_52559_relay") - assert state - assert state.state == STATE_ON + """Test Stretch switch snapshot.""" + await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) async def test_stretch_switch_changes( diff --git a/tests/components/portainer/conftest.py b/tests/components/portainer/conftest.py index 2d0f8e34d33f5a..d6127c4344020b 100644 --- a/tests/components/portainer/conftest.py +++ b/tests/components/portainer/conftest.py @@ -8,13 +8,14 @@ import pytest from homeassistant.components.portainer.const import DOMAIN -from homeassistant.const import CONF_API_KEY, CONF_HOST +from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_VERIFY_SSL from tests.common import MockConfigEntry, load_json_array_fixture MOCK_TEST_CONFIG = { CONF_HOST: "https://127.0.0.1:9000/", CONF_API_KEY: "test_api_key", + CONF_VERIFY_SSL: True, } diff --git a/tests/components/squeezebox/conftest.py b/tests/components/squeezebox/conftest.py index 2dd9403d53f326..516a574658d3d1 100644 --- a/tests/components/squeezebox/conftest.py +++ b/tests/components/squeezebox/conftest.py @@ -368,70 +368,39 @@ async def configure_squeezebox_media_player_button_platform( await hass.async_block_till_done(wait_background_tasks=True) -async def configure_squeezebox_switch_platform( - hass: HomeAssistant, - config_entry: MockConfigEntry, - lms: MagicMock, -) -> None: - """Configure a squeezebox config entry with appropriate mocks for switch.""" - with ( - patch( - "homeassistant.components.squeezebox.PLATFORMS", - [Platform.SWITCH], - ), - patch("homeassistant.components.squeezebox.Server", return_value=lms), - ): - # Set up the switch platform. +@pytest.fixture +async def setup_squeezebox( + hass: HomeAssistant, config_entry: MockConfigEntry, lms: MagicMock +) -> MockConfigEntry: + """Fixture setting up a squeezebox config entry with one player.""" + with patch("homeassistant.components.squeezebox.Server", return_value=lms): await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done(wait_background_tasks=True) + await hass.async_block_till_done() + return config_entry @pytest.fixture -async def mock_alarms_player( +async def configured_player( hass: HomeAssistant, - config_entry: MockConfigEntry, + setup_squeezebox: MockConfigEntry, # depend on your setup fixture lms: MagicMock, -) -> MagicMock: - """Mock the alarms of a configured player.""" - players = await lms.async_get_players() - players[0].alarms = [ - { - "id": TEST_ALARM_ID, - "enabled": True, - "time": "07:00", - "dow": [0, 1, 2, 3, 4, 5, 6], - "repeat": False, - "url": "CURRENT_PLAYLIST", - "volume": 50, - }, - ] - await configure_squeezebox_switch_platform(hass, config_entry, lms) - return players[0] - - -@pytest.fixture -async def configured_player( - hass: HomeAssistant, config_entry: MockConfigEntry, lms: MagicMock -) -> MagicMock: - """Fixture mocking calls to pysqueezebox Player from a configured squeezebox.""" - await configure_squeezebox_media_player_platform(hass, config_entry, lms) - return (await lms.async_get_players())[0] - - -@pytest.fixture -async def configured_player_with_button( - hass: HomeAssistant, config_entry: MockConfigEntry, lms: MagicMock ) -> MagicMock: """Fixture mocking calls to pysqueezebox Player from a configured squeezebox.""" - await configure_squeezebox_media_player_button_platform(hass, config_entry, lms) + # At this point, setup_squeezebox has already patched Server and set up the entry return (await lms.async_get_players())[0] @pytest.fixture async def configured_players( - hass: HomeAssistant, config_entry: MockConfigEntry, lms_factory: MagicMock + hass: HomeAssistant, + config_entry: MockConfigEntry, + lms_factory: MagicMock, ) -> list[MagicMock]: - """Fixture mocking calls to two pysqueezebox Players from a configured squeezebox.""" + """Fixture mocking calls to multiple pysqueezebox Players from a configured squeezebox.""" lms = lms_factory(3, uuid=SERVER_UUIDS[0]) - await configure_squeezebox_media_player_platform(hass, config_entry, lms) + + with patch("homeassistant.components.squeezebox.Server", return_value=lms): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + return await lms.async_get_players() diff --git a/tests/components/squeezebox/test_button.py b/tests/components/squeezebox/test_button.py index 53c4e9ef6264a8..b1df528c28358c 100644 --- a/tests/components/squeezebox/test_button.py +++ b/tests/components/squeezebox/test_button.py @@ -1,14 +1,23 @@ """Tests for the squeezebox button component.""" -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch + +import pytest from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS -from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.core import HomeAssistant +@pytest.fixture(autouse=True) +def squeezebox_button_platform(): + """Only set up the media_player platform for squeezebox tests.""" + with patch("homeassistant.components.squeezebox.PLATFORMS", [Platform.BUTTON]): + yield + + async def test_squeezebox_press( - hass: HomeAssistant, configured_player_with_button: MagicMock + hass: HomeAssistant, configured_player: MagicMock ) -> None: """Test press service call.""" await hass.services.async_call( @@ -18,6 +27,4 @@ async def test_squeezebox_press( blocking=True, ) - configured_player_with_button.async_query.assert_called_with( - "button", "preset_1.single" - ) + configured_player.async_query.assert_called_with("button", "preset_1.single") diff --git a/tests/components/squeezebox/test_config_flow.py b/tests/components/squeezebox/test_config_flow.py index cae3672061b642..32c7558530c176 100644 --- a/tests/components/squeezebox/test_config_flow.py +++ b/tests/components/squeezebox/test_config_flow.py @@ -15,6 +15,8 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo from tests.common import MockConfigEntry @@ -411,19 +413,29 @@ async def test_dhcp_discovery_no_server_found(hass: HomeAssistant) -> None: assert result["step_id"] == "user" -async def test_dhcp_discovery_existing_player(hass: HomeAssistant) -> None: +async def test_dhcp_discovery_existing_player( + hass: HomeAssistant, entity_registry: er.EntityRegistry +) -> None: """Test that we properly ignore known players during dhcp discover.""" - with patch( - "homeassistant.helpers.entity_registry.EntityRegistry.async_get_entity_id", - return_value="test_entity", - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_DHCP}, - data=DhcpServiceInfo( - ip="1.1.1.1", - macaddress="aabbccddeeff", - hostname="any", - ), - ) - assert result["type"] is FlowResultType.ABORT + + # Register a squeezebox media_player entity with the same MAC unique_id + entity_registry.async_get_or_create( + domain="media_player", + platform=DOMAIN, + unique_id=format_mac("aabbccddeeff"), + ) + + # Now fire a DHCP discovery for the same MAC + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=DhcpServiceInfo( + ip="1.1.1.1", + macaddress="aabbccddeeff", + hostname="any", + ), + ) + + # Because the player is already known, the flow should abort + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/squeezebox/test_init.py b/tests/components/squeezebox/test_init.py index 5cb7e19abb538e..a39a30200381b8 100644 --- a/tests/components/squeezebox/test_init.py +++ b/tests/components/squeezebox/test_init.py @@ -3,10 +3,12 @@ from http import HTTPStatus from unittest.mock import MagicMock, patch +import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components.squeezebox.const import DOMAIN from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceRegistry @@ -15,6 +17,15 @@ from tests.common import MockConfigEntry +@pytest.fixture(autouse=True) +def squeezebox_media_player_platform(): + """Only set up the media_player platform for squeezebox tests.""" + with patch( + "homeassistant.components.squeezebox.PLATFORMS", [Platform.MEDIA_PLAYER] + ): + yield + + async def test_init_api_fail( hass: HomeAssistant, config_entry: MockConfigEntry, diff --git a/tests/components/squeezebox/test_media_player.py b/tests/components/squeezebox/test_media_player.py index 6e3e5be04592d2..d04e68f25189c7 100644 --- a/tests/components/squeezebox/test_media_player.py +++ b/tests/components/squeezebox/test_media_player.py @@ -65,22 +65,27 @@ SERVICE_VOLUME_UP, STATE_UNAVAILABLE, STATE_UNKNOWN, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers.entity_registry import EntityRegistry from homeassistant.util.dt import utcnow -from .conftest import ( - FAKE_VALID_ITEM_ID, - TEST_MAC, - TEST_VOLUME_STEP, - configure_squeezebox_media_player_platform, -) +from .conftest import FAKE_VALID_ITEM_ID, TEST_MAC, TEST_VOLUME_STEP from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform +@pytest.fixture(autouse=True) +def squeezebox_media_player_platform(): + """Only set up the media_player platform for squeezebox tests.""" + with patch( + "homeassistant.components.squeezebox.PLATFORMS", [Platform.MEDIA_PLAYER] + ): + yield + + async def test_entity_registry( hass: HomeAssistant, entity_registry: EntityRegistry, @@ -98,10 +103,11 @@ async def test_squeezebox_new_player_discovery( lms: MagicMock, player_factory: MagicMock, freezer: FrozenDateTimeFactory, + setup_squeezebox: MockConfigEntry, ) -> None: """Test discovery of a new squeezebox player.""" # Initial setup with one player (from the 'lms' fixture) - await configure_squeezebox_media_player_platform(hass, config_entry, lms) + # await setup_squeezebox await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get("media_player.test_player") is not None assert hass.states.get("media_player.test_player_2") is None diff --git a/tests/components/squeezebox/test_switch.py b/tests/components/squeezebox/test_switch.py index 2e6e9bafeb05b0..368fa1bf84ac1d 100644 --- a/tests/components/squeezebox/test_switch.py +++ b/tests/components/squeezebox/test_switch.py @@ -1,14 +1,20 @@ """Tests for the Squeezebox alarm switch platform.""" from datetime import timedelta -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch from freezegun.api import FrozenDateTimeFactory +import pytest from syrupy.assertion import SnapshotAssertion from homeassistant.components.squeezebox.const import PLAYER_UPDATE_INTERVAL from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.const import CONF_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.const import ( + CONF_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import EntityRegistry @@ -17,6 +23,40 @@ from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform +@pytest.fixture(autouse=True) +def squeezebox_alarm_platform(): + """Only set up the media_player platform for squeezebox tests.""" + with patch("homeassistant.components.squeezebox.PLATFORMS", [Platform.SWITCH]): + yield + + +@pytest.fixture +async def mock_alarms_player( + hass: HomeAssistant, + config_entry: MockConfigEntry, + lms: MagicMock, +) -> MagicMock: + """Mock the alarms of a configured player.""" + players = await lms.async_get_players() + players[0].alarms = [ + { + "id": TEST_ALARM_ID, + "enabled": True, + "time": "07:00", + "dow": [0, 1, 2, 3, 4, 5, 6], + "repeat": False, + "url": "CURRENT_PLAYLIST", + "volume": 50, + }, + ] + + with patch("homeassistant.components.squeezebox.Server", return_value=lms): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + return players[0] + + async def test_entity_registry( hass: HomeAssistant, entity_registry: EntityRegistry, diff --git a/tests/components/thread/test_config_flow.py b/tests/components/thread/test_config_flow.py index 7feefdafedf900..1f9561ac4c7d01 100644 --- a/tests/components/thread/test_config_flow.py +++ b/tests/components/thread/test_config_flow.py @@ -3,6 +3,8 @@ from ipaddress import ip_address from unittest.mock import patch +import pytest + from homeassistant.components import thread from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -56,14 +58,18 @@ async def test_import(hass: HomeAssistant) -> None: assert config_entry.unique_id is None -async def test_import_then_zeroconf(hass: HomeAssistant) -> None: - """Test the import flow.""" +@pytest.mark.parametrize("source", ["import", "user"]) +async def test_single_instance_allowed_zeroconf( + hass: HomeAssistant, + source: str, +) -> None: + """Test zeroconf single instance allowed abort reason.""" with patch( "homeassistant.components.thread.async_setup_entry", return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( - thread.DOMAIN, context={"source": "import"} + thread.DOMAIN, context={"source": source} ) assert result["type"] is FlowResultType.CREATE_ENTRY @@ -77,7 +83,7 @@ async def test_import_then_zeroconf(hass: HomeAssistant) -> None: ) assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" + assert result["reason"] == "single_instance_allowed" assert len(mock_setup_entry.mock_calls) == 0 @@ -152,8 +158,45 @@ async def test_zeroconf_setup_onboarding(hass: HomeAssistant) -> None: assert len(mock_setup_entry.mock_calls) == 1 -async def test_zeroconf_then_import(hass: HomeAssistant) -> None: - """Test the import flow.""" +@pytest.mark.parametrize( + ("first_source", "second_source"), [("import", "user"), ("user", "import")] +) +async def test_import_and_user( + hass: HomeAssistant, + first_source: str, + second_source: str, +) -> None: + """Test single instance allowed for user and import.""" + with patch( + "homeassistant.components.thread.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + thread.DOMAIN, context={"source": first_source} + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert len(mock_setup_entry.mock_calls) == 1 + + with patch( + "homeassistant.components.thread.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + thread.DOMAIN, context={"source": second_source} + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "single_instance_allowed" + assert len(mock_setup_entry.mock_calls) == 0 + + +@pytest.mark.parametrize("source", ["import", "user"]) +async def test_zeroconf_then_import_user( + hass: HomeAssistant, + source: str, +) -> None: + """Test single instance allowed abort reason for import/user flow.""" result = await hass.config_entries.flow.async_init( thread.DOMAIN, context={"source": "zeroconf"}, data=TEST_ZEROCONF_RECORD ) @@ -169,9 +212,37 @@ async def test_zeroconf_then_import(hass: HomeAssistant) -> None: return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( - thread.DOMAIN, context={"source": "import"} + thread.DOMAIN, context={"source": source} ) assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "already_configured" + assert result["reason"] == "single_instance_allowed" assert len(mock_setup_entry.mock_calls) == 0 + + +@pytest.mark.parametrize("source", ["import", "user"]) +async def test_zeroconf_in_progress_then_import_user( + hass: HomeAssistant, + source: str, +) -> None: + """Test priority (import/user) flow with zeroconf flow in progress.""" + result = await hass.config_entries.flow.async_init( + thread.DOMAIN, context={"source": "zeroconf"}, data=TEST_ZEROCONF_RECORD + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "confirm" + + with patch( + "homeassistant.components.thread.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + thread.DOMAIN, context={"source": source} + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert mock_setup_entry.call_count == 1 + + flows_in_progress = hass.config_entries.flow.async_progress() + assert len(flows_in_progress) == 0