diff --git a/homeassistant/components/esphome/light.py b/homeassistant/components/esphome/light.py index 3c1499cf1ff..d8d827f18a1 100644 --- a/homeassistant/components/esphome/light.py +++ b/homeassistant/components/esphome/light.py @@ -109,7 +109,7 @@ def _mired_to_kelvin(mired_temperature: float) -> int: def _color_mode_to_ha(mode: int) -> str: """Convert an esphome color mode to a HA color mode constant. - Chose the color mode that best matches the feature-set. + Choose the color mode that best matches the feature-set. """ candidates = [] for ha_mode, cap_lists in _COLOR_MODE_MAPPING.items(): diff --git a/homeassistant/components/esphome/media_player.py b/homeassistant/components/esphome/media_player.py index b05a453aca2..3af6c0b2049 100644 --- a/homeassistant/components/esphome/media_player.py +++ b/homeassistant/components/esphome/media_player.py @@ -96,7 +96,7 @@ class EsphomeMediaPlayer( @property @esphome_float_state_property - def volume_level(self) -> float | None: + def volume_level(self) -> float: """Volume level of the media player (0..1).""" return self._state.volume diff --git a/homeassistant/components/esphome/switch.py b/homeassistant/components/esphome/switch.py index 96b2a426869..35edbf678ad 100644 --- a/homeassistant/components/esphome/switch.py +++ b/homeassistant/components/esphome/switch.py @@ -36,7 +36,7 @@ class EsphomeSwitch(EsphomeEntity[SwitchInfo, SwitchState], SwitchEntity): @property @esphome_state_property - def is_on(self) -> bool | None: + def is_on(self) -> bool: """Return true if the switch is on.""" return self._state.state diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py index 9125e92a552..d2c8d9dc3d0 100644 --- a/homeassistant/components/esphome/update.py +++ b/homeassistant/components/esphome/update.py @@ -109,7 +109,6 @@ class ESPHomeDashboardUpdateEntity( _attr_has_entity_name = True _attr_device_class = UpdateDeviceClass.FIRMWARE _attr_title = "ESPHome" - _attr_name = "Firmware" _attr_release_url = "https://esphome.io/changelog/" _attr_entity_registry_enabled_default = False @@ -242,7 +241,7 @@ class ESPHomeUpdateEntity(EsphomeEntity[UpdateInfo, UpdateState], UpdateEntity): @property @esphome_state_property - def installed_version(self) -> str | None: + def installed_version(self) -> str: """Return the installed version.""" return self._state.current_version @@ -260,19 +259,19 @@ class ESPHomeUpdateEntity(EsphomeEntity[UpdateInfo, UpdateState], UpdateEntity): @property @esphome_state_property - def release_summary(self) -> str | None: + def release_summary(self) -> str: """Return the release summary.""" return self._state.release_summary @property @esphome_state_property - def release_url(self) -> str | None: + def release_url(self) -> str: """Return the release URL.""" return self._state.release_url @property @esphome_state_property - def title(self) -> str | None: + def title(self) -> str: """Return the title of the update.""" return self._state.title diff --git a/homeassistant/components/esphome/valve.py b/homeassistant/components/esphome/valve.py index e366fc08d19..f71a253c1f1 100644 --- a/homeassistant/components/esphome/valve.py +++ b/homeassistant/components/esphome/valve.py @@ -65,7 +65,7 @@ class EsphomeValve(EsphomeEntity[ValveInfo, ValveState], ValveEntity): @property @esphome_state_property - def current_valve_position(self) -> int | None: + def current_valve_position(self) -> int: """Return current position of valve. 0 is closed, 100 is open.""" return round(self._state.position * 100.0) diff --git a/tests/components/esphome/common.py b/tests/components/esphome/common.py new file mode 100644 index 00000000000..39661c0f340 --- /dev/null +++ b/tests/components/esphome/common.py @@ -0,0 +1,34 @@ +"""ESPHome test common code.""" + +from homeassistant.components import assist_satellite +from homeassistant.components.assist_satellite import AssistSatelliteEntity + +# pylint: disable-next=hass-component-root-import +from homeassistant.components.esphome import DOMAIN +from homeassistant.components.esphome.assist_satellite import EsphomeAssistSatellite +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_component import EntityComponent + + +def get_satellite_entity( + hass: HomeAssistant, mac_address: str +) -> EsphomeAssistSatellite | None: + """Get the satellite entity for a device.""" + ent_reg = er.async_get(hass) + satellite_entity_id = ent_reg.async_get_entity_id( + Platform.ASSIST_SATELLITE, DOMAIN, f"{mac_address}-assist_satellite" + ) + if satellite_entity_id is None: + return None + assert satellite_entity_id.endswith("_assist_satellite") + + component: EntityComponent[AssistSatelliteEntity] = hass.data[ + assist_satellite.DOMAIN + ] + if (entity := component.get_entity(satellite_entity_id)) is not None: + assert isinstance(entity, EsphomeAssistSatellite) + return entity + + return None diff --git a/tests/components/esphome/test_assist_satellite.py b/tests/components/esphome/test_assist_satellite.py index c072e5fda4a..dddbbcc45f1 100644 --- a/tests/components/esphome/test_assist_satellite.py +++ b/tests/components/esphome/test_assist_satellite.py @@ -34,59 +34,28 @@ from homeassistant.components import ( from homeassistant.components.assist_pipeline import PipelineEvent, PipelineEventType from homeassistant.components.assist_satellite import ( AssistSatelliteConfiguration, - AssistSatelliteEntity, AssistSatelliteEntityFeature, AssistSatelliteWakeWord, ) # pylint: disable-next=hass-component-root-import from homeassistant.components.assist_satellite.entity import AssistSatelliteState -from homeassistant.components.esphome import DOMAIN -from homeassistant.components.esphome.assist_satellite import ( - EsphomeAssistSatellite, - VoiceAssistantUDPServer, -) +from homeassistant.components.esphome.assist_satellite import VoiceAssistantUDPServer from homeassistant.components.select import ( DOMAIN as SELECT_DOMAIN, SERVICE_SELECT_OPTION, ) -from homeassistant.const import STATE_UNAVAILABLE, Platform +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant -from homeassistant.helpers import ( - device_registry as dr, - entity_registry as er, - intent as intent_helper, -) -from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers import device_registry as dr, intent as intent_helper from homeassistant.helpers.network import get_url +from .common import get_satellite_entity from .conftest import MockESPHomeDevice from tests.components.tts.common import MockResultStream -def get_satellite_entity( - hass: HomeAssistant, mac_address: str -) -> EsphomeAssistSatellite | None: - """Get the satellite entity for a device.""" - ent_reg = er.async_get(hass) - satellite_entity_id = ent_reg.async_get_entity_id( - Platform.ASSIST_SATELLITE, DOMAIN, f"{mac_address}-assist_satellite" - ) - if satellite_entity_id is None: - return None - assert satellite_entity_id.endswith("_assist_satellite") - - component: EntityComponent[AssistSatelliteEntity] = hass.data[ - assist_satellite.DOMAIN - ] - if (entity := component.get_entity(satellite_entity_id)) is not None: - assert isinstance(entity, EsphomeAssistSatellite) - return entity - - return None - - @pytest.fixture def mock_wav() -> bytes: """Return test WAV audio.""" @@ -1143,32 +1112,6 @@ async def test_tts_minimal_format_from_media_player( } -async def test_announce_supported_features( - hass: HomeAssistant, - mock_client: APIClient, - mock_esphome_device: Callable[ - [APIClient, list[EntityInfo], list[UserService], list[EntityState]], - Awaitable[MockESPHomeDevice], - ], -) -> None: - """Test that the announce supported feature is not set by default.""" - mock_device: MockESPHomeDevice = await mock_esphome_device( - mock_client=mock_client, - entity_info=[], - user_service=[], - states=[], - device_info={ - "voice_assistant_feature_flags": VoiceAssistantFeature.VOICE_ASSISTANT - }, - ) - await hass.async_block_till_done() - - satellite = get_satellite_entity(hass, mock_device.device_info.mac_address) - assert satellite is not None - - assert not (satellite.supported_features & AssistSatelliteEntityFeature.ANNOUNCE) - - async def test_announce_message( hass: HomeAssistant, mock_client: APIClient, @@ -1236,7 +1179,7 @@ async def test_announce_message( assist_satellite.DOMAIN, "announce", { - "entity_id": satellite.entity_id, + ATTR_ENTITY_ID: satellite.entity_id, "message": "test-text", "preannounce": False, }, @@ -1326,7 +1269,7 @@ async def test_announce_media_id( assist_satellite.DOMAIN, "announce", { - "entity_id": satellite.entity_id, + ATTR_ENTITY_ID: satellite.entity_id, "media_id": "https://www.home-assistant.io/resolved.mp3", "preannounce": False, }, @@ -1413,7 +1356,7 @@ async def test_announce_message_with_preannounce( assist_satellite.DOMAIN, "announce", { - "entity_id": satellite.entity_id, + ATTR_ENTITY_ID: satellite.entity_id, "message": "test-text", "preannounce_media_id": "test-preannounce", }, @@ -1423,7 +1366,7 @@ async def test_announce_message_with_preannounce( assert satellite.state == AssistSatelliteState.IDLE -async def test_start_conversation_supported_features( +async def test_non_default_supported_features( hass: HomeAssistant, mock_client: APIClient, mock_esphome_device: Callable[ @@ -1431,7 +1374,7 @@ async def test_start_conversation_supported_features( Awaitable[MockESPHomeDevice], ], ) -> None: - """Test that the start conversation supported feature is not set by default.""" + """Test that the start conversation and announce are not set by default.""" mock_device: MockESPHomeDevice = await mock_esphome_device( mock_client=mock_client, entity_info=[], @@ -1449,6 +1392,7 @@ async def test_start_conversation_supported_features( assert not ( satellite.supported_features & AssistSatelliteEntityFeature.START_CONVERSATION ) + assert not (satellite.supported_features & AssistSatelliteEntityFeature.ANNOUNCE) async def test_start_conversation_message( @@ -1537,7 +1481,7 @@ async def test_start_conversation_message( assist_satellite.DOMAIN, "start_conversation", { - "entity_id": satellite.entity_id, + ATTR_ENTITY_ID: satellite.entity_id, "start_message": "test-text", "preannounce": False, }, @@ -1646,7 +1590,7 @@ async def test_start_conversation_media_id( assist_satellite.DOMAIN, "start_conversation", { - "entity_id": satellite.entity_id, + ATTR_ENTITY_ID: satellite.entity_id, "start_media_id": "https://www.home-assistant.io/resolved.mp3", "preannounce": False, }, @@ -1752,7 +1696,7 @@ async def test_start_conversation_message_with_preannounce( assist_satellite.DOMAIN, "start_conversation", { - "entity_id": satellite.entity_id, + ATTR_ENTITY_ID: satellite.entity_id, "start_message": "test-text", "preannounce_media_id": "test-preannounce", }, @@ -1982,7 +1926,7 @@ async def test_wake_word_select( await hass.services.async_call( SELECT_DOMAIN, SERVICE_SELECT_OPTION, - {"entity_id": "select.test_wake_word", "option": "Okay Nabu"}, + {ATTR_ENTITY_ID: "select.test_wake_word", "option": "Okay Nabu"}, blocking=True, ) await hass.async_block_till_done() @@ -1997,122 +1941,3 @@ async def test_wake_word_select( # Satellite config should have been updated assert satellite.async_get_configuration().active_wake_words == ["okay_nabu"] - - -async def test_wake_word_select_no_wake_words( - hass: HomeAssistant, - mock_client: APIClient, - mock_esphome_device: Callable[ - [APIClient, list[EntityInfo], list[UserService], list[EntityState]], - Awaitable[MockESPHomeDevice], - ], -) -> None: - """Test wake word select is unavailable when there are no available wake word.""" - device_config = AssistSatelliteConfiguration( - available_wake_words=[], - active_wake_words=[], - max_active_wake_words=1, - ) - mock_client.get_voice_assistant_configuration.return_value = device_config - - mock_device: MockESPHomeDevice = await mock_esphome_device( - mock_client=mock_client, - entity_info=[], - user_service=[], - states=[], - device_info={ - "voice_assistant_feature_flags": VoiceAssistantFeature.VOICE_ASSISTANT - | VoiceAssistantFeature.ANNOUNCE - }, - ) - await hass.async_block_till_done() - - satellite = get_satellite_entity(hass, mock_device.device_info.mac_address) - assert satellite is not None - assert not satellite.async_get_configuration().available_wake_words - - # Select should be unavailable - state = hass.states.get("select.test_wake_word") - assert state is not None - assert state.state == STATE_UNAVAILABLE - - -async def test_wake_word_select_zero_max_wake_words( - hass: HomeAssistant, - mock_client: APIClient, - mock_esphome_device: Callable[ - [APIClient, list[EntityInfo], list[UserService], list[EntityState]], - Awaitable[MockESPHomeDevice], - ], -) -> None: - """Test wake word select is unavailable max wake words is zero.""" - device_config = AssistSatelliteConfiguration( - available_wake_words=[ - AssistSatelliteWakeWord("okay_nabu", "Okay Nabu", ["en"]), - ], - active_wake_words=[], - max_active_wake_words=0, - ) - mock_client.get_voice_assistant_configuration.return_value = device_config - - mock_device: MockESPHomeDevice = await mock_esphome_device( - mock_client=mock_client, - entity_info=[], - user_service=[], - states=[], - device_info={ - "voice_assistant_feature_flags": VoiceAssistantFeature.VOICE_ASSISTANT - | VoiceAssistantFeature.ANNOUNCE - }, - ) - await hass.async_block_till_done() - - satellite = get_satellite_entity(hass, mock_device.device_info.mac_address) - assert satellite is not None - assert satellite.async_get_configuration().max_active_wake_words == 0 - - # Select should be unavailable - state = hass.states.get("select.test_wake_word") - assert state is not None - assert state.state == STATE_UNAVAILABLE - - -async def test_wake_word_select_no_active_wake_words( - hass: HomeAssistant, - mock_client: APIClient, - mock_esphome_device: Callable[ - [APIClient, list[EntityInfo], list[UserService], list[EntityState]], - Awaitable[MockESPHomeDevice], - ], -) -> None: - """Test wake word select uses first available wake word if none are active.""" - device_config = AssistSatelliteConfiguration( - available_wake_words=[ - AssistSatelliteWakeWord("okay_nabu", "Okay Nabu", ["en"]), - AssistSatelliteWakeWord("hey_jarvis", "Hey Jarvis", ["en"]), - ], - active_wake_words=[], - max_active_wake_words=1, - ) - mock_client.get_voice_assistant_configuration.return_value = device_config - - mock_device: MockESPHomeDevice = await mock_esphome_device( - mock_client=mock_client, - entity_info=[], - user_service=[], - states=[], - device_info={ - "voice_assistant_feature_flags": VoiceAssistantFeature.VOICE_ASSISTANT - | VoiceAssistantFeature.ANNOUNCE - }, - ) - await hass.async_block_till_done() - - satellite = get_satellite_entity(hass, mock_device.device_info.mac_address) - assert satellite is not None - assert not satellite.async_get_configuration().active_wake_words - - # First available wake word should be selected - state = hass.states.get("select.test_wake_word") - assert state is not None - assert state.state == "Okay Nabu" diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 752f980cd87..3e58244707d 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -72,18 +72,16 @@ async def test_user_connection_works( ) -> None: """Test we can finish a config flow.""" result = await hass.config_entries.flow.async_init( - "esphome", + DOMAIN, context={"source": config_entries.SOURCE_USER}, - data=None, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" - result = await hass.config_entries.flow.async_init( - "esphome", - context={"source": config_entries.SOURCE_USER}, - data={CONF_HOST: "127.0.0.1", CONF_PORT: 80}, + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "127.0.0.1", CONF_PORT: 80}, ) assert result["type"] is FlowResultType.CREATE_ENTRY @@ -119,7 +117,7 @@ async def test_user_connection_updates_host(hass: HomeAssistant) -> None: ) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - "esphome", + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None, ) @@ -127,10 +125,9 @@ async def test_user_connection_updates_host(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" - result = await hass.config_entries.flow.async_init( - "esphome", - context={"source": config_entries.SOURCE_USER}, - data={CONF_HOST: "127.0.0.1", CONF_PORT: 80}, + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_HOST: "127.0.0.1", CONF_PORT: 80}, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured_updates" @@ -157,7 +154,7 @@ async def test_user_sets_unique_id(hass: HomeAssistant) -> None: type="mock_type", ) discovery_result = await hass.config_entries.flow.async_init( - "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) assert discovery_result["type"] is FlowResultType.FORM @@ -180,7 +177,7 @@ async def test_user_sets_unique_id(hass: HomeAssistant) -> None: } result = await hass.config_entries.flow.async_init( - "esphome", + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None, ) @@ -211,7 +208,7 @@ async def test_user_resolve_error(hass: HomeAssistant, mock_client: APIClient) - ) as exc: mock_client.device_info.side_effect = exc result = await hass.config_entries.flow.async_init( - "esphome", + DOMAIN, context={"source": config_entries.SOURCE_USER}, data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) @@ -258,7 +255,7 @@ async def test_user_causes_zeroconf_to_abort(hass: HomeAssistant) -> None: type="mock_type", ) discovery_result = await hass.config_entries.flow.async_init( - "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) assert discovery_result["type"] is FlowResultType.FORM @@ -268,7 +265,7 @@ async def test_user_causes_zeroconf_to_abort(hass: HomeAssistant) -> None: } result = await hass.config_entries.flow.async_init( - "esphome", + DOMAIN, context={"source": config_entries.SOURCE_USER}, data=None, ) @@ -301,7 +298,7 @@ async def test_user_connection_error( mock_client.device_info.side_effect = APIConnectionError result = await hass.config_entries.flow.async_init( - "esphome", + DOMAIN, context={"source": config_entries.SOURCE_USER}, data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) @@ -342,7 +339,7 @@ async def test_user_with_password( mock_client.device_info.return_value = DeviceInfo(uses_password=True, name="test") result = await hass.config_entries.flow.async_init( - "esphome", + DOMAIN, context={"source": config_entries.SOURCE_USER}, data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) @@ -374,7 +371,7 @@ async def test_user_invalid_password( mock_client.device_info.return_value = DeviceInfo(uses_password=True, name="test") result = await hass.config_entries.flow.async_init( - "esphome", + DOMAIN, context={"source": config_entries.SOURCE_USER}, data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) @@ -432,7 +429,7 @@ async def test_user_dashboard_has_wrong_key( return_value=WRONG_NOISE_PSK, ): result = await hass.config_entries.flow.async_init( - "esphome", + DOMAIN, context={"source": config_entries.SOURCE_USER}, data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) @@ -487,7 +484,7 @@ async def test_user_discovers_name_and_gets_key_from_dashboard( return_value=VALID_NOISE_PSK, ): result = await hass.config_entries.flow.async_init( - "esphome", + DOMAIN, context={"source": config_entries.SOURCE_USER}, data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) @@ -539,7 +536,7 @@ async def test_user_discovers_name_and_gets_key_from_dashboard_fails( side_effect=dashboard_exception, ): result = await hass.config_entries.flow.async_init( - "esphome", + DOMAIN, context={"source": config_entries.SOURCE_USER}, data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) @@ -589,12 +586,12 @@ async def test_user_discovers_name_and_dashboard_is_unavailable( ) with patch( - "esphome_dashboard_api.ESPHomeDashboardAPI.get_devices", + "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.get_devices", side_effect=TimeoutError, ): await dashboard.async_get_dashboard(hass).async_refresh() result = await hass.config_entries.flow.async_init( - "esphome", + DOMAIN, context={"source": config_entries.SOURCE_USER}, data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) @@ -627,7 +624,7 @@ async def test_login_connection_error( mock_client.device_info.return_value = DeviceInfo(uses_password=True, name="test") result = await hass.config_entries.flow.async_init( - "esphome", + DOMAIN, context={"source": config_entries.SOURCE_USER}, data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) @@ -680,7 +677,7 @@ async def test_discovery_initiation(hass: HomeAssistant) -> None: type="mock_type", ) flow = await hass.config_entries.flow.async_init( - "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) assert get_flow_context(hass, flow) == { "source": config_entries.SOURCE_ZEROCONF, @@ -714,7 +711,7 @@ async def test_discovery_no_mac(hass: HomeAssistant) -> None: type="mock_type", ) flow = await hass.config_entries.flow.async_init( - "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) assert flow["type"] is FlowResultType.ABORT assert flow["reason"] == "mdns_missing_mac" @@ -741,7 +738,7 @@ async def test_discovery_already_configured(hass: HomeAssistant) -> None: type="mock_type", ) result = await hass.config_entries.flow.async_init( - "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) assert result["type"] is FlowResultType.ABORT @@ -767,14 +764,14 @@ async def test_discovery_duplicate_data(hass: HomeAssistant) -> None: ) result = await hass.config_entries.flow.async_init( - "esphome", data=service_info, context={"source": config_entries.SOURCE_ZEROCONF} + DOMAIN, data=service_info, context={"source": config_entries.SOURCE_ZEROCONF} ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "discovery_confirm" assert result["description_placeholders"] == {"name": "test"} result = await hass.config_entries.flow.async_init( - "esphome", data=service_info, context={"source": config_entries.SOURCE_ZEROCONF} + DOMAIN, data=service_info, context={"source": config_entries.SOURCE_ZEROCONF} ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_in_progress" @@ -801,7 +798,7 @@ async def test_discovery_updates_unique_id(hass: HomeAssistant) -> None: type="mock_type", ) result = await hass.config_entries.flow.async_init( - "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) assert result["type"] is FlowResultType.ABORT @@ -821,7 +818,7 @@ async def test_user_requires_psk(hass: HomeAssistant, mock_client: APIClient) -> mock_client.device_info.side_effect = RequiresEncryptionAPIError result = await hass.config_entries.flow.async_init( - "esphome", + DOMAIN, context={"source": config_entries.SOURCE_USER}, data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) @@ -867,7 +864,7 @@ async def test_encryption_key_valid_psk( mock_client.device_info.side_effect = RequiresEncryptionAPIError result = await hass.config_entries.flow.async_init( - "esphome", + DOMAIN, context={"source": config_entries.SOURCE_USER}, data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) @@ -903,7 +900,7 @@ async def test_encryption_key_invalid_psk( mock_client.device_info.side_effect = RequiresEncryptionAPIError result = await hass.config_entries.flow.async_init( - "esphome", + DOMAIN, context={"source": config_entries.SOURCE_USER}, data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) @@ -1301,7 +1298,7 @@ async def test_discovery_dhcp_updates_host( macaddress="1122334455aa", ) result = await hass.config_entries.flow.async_init( - "esphome", context={"source": config_entries.SOURCE_DHCP}, data=service_info + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=service_info ) assert result["type"] is FlowResultType.ABORT @@ -1337,7 +1334,7 @@ async def test_discovery_dhcp_does_not_update_host_wrong_mac( macaddress="1122334455aa", ) result = await hass.config_entries.flow.async_init( - "esphome", context={"source": config_entries.SOURCE_DHCP}, data=service_info + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=service_info ) assert result["type"] is FlowResultType.ABORT @@ -1372,7 +1369,7 @@ async def test_discovery_dhcp_does_not_update_host_wrong_mac_bad_key( macaddress="1122334455aa", ) result = await hass.config_entries.flow.async_init( - "esphome", context={"source": config_entries.SOURCE_DHCP}, data=service_info + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=service_info ) assert result["type"] is FlowResultType.ABORT @@ -1407,7 +1404,7 @@ async def test_discovery_dhcp_does_not_update_host_missing_mac_bad_key( macaddress="1122334455aa", ) result = await hass.config_entries.flow.async_init( - "esphome", context={"source": config_entries.SOURCE_DHCP}, data=service_info + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=service_info ) assert result["type"] is FlowResultType.ABORT @@ -1441,7 +1438,7 @@ async def test_discovery_dhcp_no_changes( macaddress="000000000000", ) result = await hass.config_entries.flow.async_init( - "esphome", context={"source": config_entries.SOURCE_DHCP}, data=service_info + DOMAIN, context={"source": config_entries.SOURCE_DHCP}, data=service_info ) assert result["type"] is FlowResultType.ABORT @@ -1454,7 +1451,7 @@ async def test_discovery_dhcp_no_changes( async def test_discovery_hassio(hass: HomeAssistant) -> None: """Test dashboard discovery.""" result = await hass.config_entries.flow.async_init( - "esphome", + DOMAIN, data=HassioServiceInfo( config={ "host": "mock-esphome", @@ -1494,7 +1491,7 @@ async def test_zeroconf_encryption_key_via_dashboard( type="mock_type", ) flow = await hass.config_entries.flow.async_init( - "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) assert flow["type"] is FlowResultType.FORM @@ -1561,7 +1558,7 @@ async def test_zeroconf_encryption_key_via_dashboard_with_api_encryption_prop( type="mock_type", ) flow = await hass.config_entries.flow.async_init( - "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) assert flow["type"] is FlowResultType.FORM @@ -1625,7 +1622,7 @@ async def test_zeroconf_no_encryption_key_via_dashboard( type="mock_type", ) flow = await hass.config_entries.flow.async_init( - "esphome", context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info + DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=service_info ) assert flow["type"] is FlowResultType.FORM @@ -1767,7 +1764,7 @@ async def test_user_discovers_name_no_dashboard( ] result = await hass.config_entries.flow.async_init( - "esphome", + DOMAIN, context={"source": config_entries.SOURCE_USER}, data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) @@ -1805,7 +1802,7 @@ async def mqtt_discovery_test_abort( timestamp=None, ) flow = await hass.config_entries.flow.async_init( - "esphome", context={"source": config_entries.SOURCE_MQTT}, data=service_info + DOMAIN, context={"source": config_entries.SOURCE_MQTT}, data=service_info ) assert flow["type"] is FlowResultType.ABORT assert flow["reason"] == reason @@ -1849,7 +1846,7 @@ async def test_discovery_mqtt_initiation(hass: HomeAssistant) -> None: timestamp=None, ) flow = await hass.config_entries.flow.async_init( - "esphome", context={"source": config_entries.SOURCE_MQTT}, data=service_info + DOMAIN, context={"source": config_entries.SOURCE_MQTT}, data=service_info ) result = await hass.config_entries.flow.async_configure( @@ -1886,7 +1883,7 @@ async def test_user_flow_name_conflict_migrate( ) result = await hass.config_entries.flow.async_init( - "esphome", + DOMAIN, context={"source": config_entries.SOURCE_USER}, data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) @@ -1936,11 +1933,10 @@ async def test_user_flow_name_conflict_overwrite( ) result = await hass.config_entries.flow.async_init( - "esphome", + DOMAIN, context={"source": config_entries.SOURCE_USER}, data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, ) - await hass.async_block_till_done() assert result["type"] is FlowResultType.MENU assert result["step_id"] == "name_conflict" diff --git a/tests/components/esphome/test_dashboard.py b/tests/components/esphome/test_dashboard.py index 1f675a10b82..5fa53dc7f75 100644 --- a/tests/components/esphome/test_dashboard.py +++ b/tests/components/esphome/test_dashboard.py @@ -6,12 +6,7 @@ from unittest.mock import patch from aioesphomeapi import DeviceInfo, InvalidAuthAPIError import pytest -from homeassistant.components.esphome import ( - CONF_NOISE_PSK, - DOMAIN, - coordinator, - dashboard, -) +from homeassistant.components.esphome import CONF_NOISE_PSK, DOMAIN, dashboard from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -117,8 +112,9 @@ async def test_setup_dashboard_fails( hass_storage: dict[str, Any], ) -> None: """Test that nothing is stored on failed dashboard setup when there was no dashboard before.""" - with patch.object( - coordinator.ESPHomeDashboardAPI, "get_devices", side_effect=TimeoutError + with patch( + "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.get_devices", + side_effect=TimeoutError, ) as mock_get_devices: await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() @@ -136,8 +132,8 @@ async def test_setup_dashboard_fails_when_already_setup( hass_storage: dict[str, Any], ) -> None: """Test failed dashboard setup still reloads entries if one existed before.""" - with patch.object( - coordinator.ESPHomeDashboardAPI, "get_devices" + with patch( + "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.get_devices" ) as mock_get_devices: await dashboard.async_set_dashboard_info( hass, "test-slug", "working-host", 6052 @@ -151,8 +147,9 @@ async def test_setup_dashboard_fails_when_already_setup( await hass.async_block_till_done() with ( - patch.object( - coordinator.ESPHomeDashboardAPI, "get_devices", side_effect=TimeoutError + patch( + "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.get_devices", + side_effect=TimeoutError, ) as mock_get_devices, patch( "homeassistant.components.esphome.async_setup_entry", return_value=True diff --git a/tests/components/esphome/test_entity.py b/tests/components/esphome/test_entity.py index 290b1871cd7..1184b345d14 100644 --- a/tests/components/esphome/test_entity.py +++ b/tests/components/esphome/test_entity.py @@ -221,9 +221,6 @@ async def test_entities_removed_after_reload( unique_id="my_binary_sensor", ), ] - states = [ - BinarySensorState(key=1, state=True, missing_state=False), - ] mock_device.client.list_entities_services = AsyncMock( return_value=(entity_info, user_service) ) diff --git a/tests/components/esphome/test_entry_data.py b/tests/components/esphome/test_entry_data.py index a8535c38224..61d0688e641 100644 --- a/tests/components/esphome/test_entry_data.py +++ b/tests/components/esphome/test_entry_data.py @@ -7,6 +7,8 @@ from aioesphomeapi import ( SensorState, ) +from homeassistant.components.esphome import DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -62,15 +64,15 @@ async def test_migrate_entity_unique_id_downgrade_upgrade( ) -> None: """Test unique id migration prefers the original entity on downgrade upgrade.""" entity_registry.async_get_or_create( - "sensor", - "esphome", + SENSOR_DOMAIN, + DOMAIN, "my_sensor", suggested_object_id="old_sensor", disabled_by=None, ) entity_registry.async_get_or_create( - "sensor", - "esphome", + SENSOR_DOMAIN, + DOMAIN, "11:22:33:44:55:AA-sensor-mysensor", suggested_object_id="new_sensor", disabled_by=None, @@ -103,7 +105,7 @@ async def test_migrate_entity_unique_id_downgrade_upgrade( # entity that was only created on downgrade and they keep # the original one. assert ( - entity_registry.async_get_entity_id("sensor", "esphome", "my_sensor") + entity_registry.async_get_entity_id(SENSOR_DOMAIN, DOMAIN, "my_sensor") is not None ) # Note that ESPHome includes the EntityInfo type in the unique id diff --git a/tests/components/esphome/test_init.py b/tests/components/esphome/test_init.py index 9e4c9709e7d..7473734ff3e 100644 --- a/tests/components/esphome/test_init.py +++ b/tests/components/esphome/test_init.py @@ -9,9 +9,9 @@ from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry -@pytest.mark.usefixtures("mock_zeroconf") -async def test_delete_entry(hass: HomeAssistant, mock_client) -> None: - """Test we can delete an entry with error.""" +@pytest.mark.usefixtures("mock_client", "mock_zeroconf") +async def test_delete_entry(hass: HomeAssistant) -> None: + """Test we can delete an entry without error.""" entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "test.local", CONF_PORT: 6053, CONF_PASSWORD: ""}, diff --git a/tests/components/esphome/test_light.py b/tests/components/esphome/test_light.py index 8e4f37079d1..e713bbbe630 100644 --- a/tests/components/esphome/test_light.py +++ b/tests/components/esphome/test_light.py @@ -38,6 +38,8 @@ from homeassistant.components.light import ( from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant +LIGHT_COLOR_CAPABILITY_UNKNOWN = 1 << 8 # 256 + async def test_light_on_off( hass: HomeAssistant, mock_client: APIClient, mock_generic_device_entry @@ -391,7 +393,9 @@ async def test_light_brightness_on_off_with_unknown_color_mode( min_mireds=153, max_mireds=400, supported_color_modes=[ - LightColorCapability.ON_OFF | LightColorCapability.BRIGHTNESS | 1 << 8 + LightColorCapability.ON_OFF + | LightColorCapability.BRIGHTNESS + | LIGHT_COLOR_CAPABILITY_UNKNOWN ], ) ] @@ -420,7 +424,7 @@ async def test_light_brightness_on_off_with_unknown_color_mode( state=True, color_mode=LightColorCapability.ON_OFF | LightColorCapability.BRIGHTNESS - | 1 << 8, + | LIGHT_COLOR_CAPABILITY_UNKNOWN, ) ] ) @@ -439,7 +443,7 @@ async def test_light_brightness_on_off_with_unknown_color_mode( state=True, color_mode=LightColorCapability.ON_OFF | LightColorCapability.BRIGHTNESS - | 1 << 8, + | LIGHT_COLOR_CAPABILITY_UNKNOWN, brightness=pytest.approx(0.4980392156862745), ) ] diff --git a/tests/components/esphome/test_manager.py b/tests/components/esphome/test_manager.py index 172b863229d..652d2453e05 100644 --- a/tests/components/esphome/test_manager.py +++ b/tests/components/esphome/test_manager.py @@ -34,6 +34,7 @@ from homeassistant.components.esphome.const import ( STABLE_BLE_VERSION_STR, ) from homeassistant.components.esphome.manager import DEVICE_CONFLICT_ISSUE_FORMAT +from homeassistant.components.tag import DOMAIN as TAG_DOMAIN from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -192,7 +193,7 @@ async def test_esphome_device_service_calls_allowed( issue_registry: ir.IssueRegistry, ) -> None: """Test a device with service calls are allowed.""" - await async_setup_component(hass, "tag", {}) + await async_setup_component(hass, TAG_DOMAIN, {}) entity_info = [] states = [] user_service = [] diff --git a/tests/components/esphome/test_select.py b/tests/components/esphome/test_select.py index 6ae1260a89d..e170a1a7f6d 100644 --- a/tests/components/esphome/test_select.py +++ b/tests/components/esphome/test_select.py @@ -1,9 +1,22 @@ """Test ESPHome selects.""" +from collections.abc import Awaitable, Callable from unittest.mock import call -from aioesphomeapi import APIClient, SelectInfo, SelectState +from aioesphomeapi import ( + APIClient, + EntityInfo, + EntityState, + SelectInfo, + SelectState, + UserService, + VoiceAssistantFeature, +) +from homeassistant.components.assist_satellite import ( + AssistSatelliteConfiguration, + AssistSatelliteWakeWord, +) from homeassistant.components.select import ( ATTR_OPTION, DOMAIN as SELECT_DOMAIN, @@ -12,6 +25,9 @@ from homeassistant.components.select import ( from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant +from .common import get_satellite_entity +from .conftest import MockESPHomeDevice + async def test_pipeline_selector( hass: HomeAssistant, @@ -80,3 +96,122 @@ async def test_select_generic_entity( blocking=True, ) mock_client.select_command.assert_has_calls([call(1, "b")]) + + +async def test_wake_word_select_no_wake_words( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], +) -> None: + """Test wake word select is unavailable when there are no available wake word.""" + device_config = AssistSatelliteConfiguration( + available_wake_words=[], + active_wake_words=[], + max_active_wake_words=1, + ) + mock_client.get_voice_assistant_configuration.return_value = device_config + + mock_device: MockESPHomeDevice = await mock_esphome_device( + mock_client=mock_client, + entity_info=[], + user_service=[], + states=[], + device_info={ + "voice_assistant_feature_flags": VoiceAssistantFeature.VOICE_ASSISTANT + | VoiceAssistantFeature.ANNOUNCE + }, + ) + await hass.async_block_till_done() + + satellite = get_satellite_entity(hass, mock_device.device_info.mac_address) + assert satellite is not None + assert not satellite.async_get_configuration().available_wake_words + + # Select should be unavailable + state = hass.states.get("select.test_wake_word") + assert state is not None + assert state.state == STATE_UNAVAILABLE + + +async def test_wake_word_select_zero_max_wake_words( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], +) -> None: + """Test wake word select is unavailable max wake words is zero.""" + device_config = AssistSatelliteConfiguration( + available_wake_words=[ + AssistSatelliteWakeWord("okay_nabu", "Okay Nabu", ["en"]), + ], + active_wake_words=[], + max_active_wake_words=0, + ) + mock_client.get_voice_assistant_configuration.return_value = device_config + + mock_device: MockESPHomeDevice = await mock_esphome_device( + mock_client=mock_client, + entity_info=[], + user_service=[], + states=[], + device_info={ + "voice_assistant_feature_flags": VoiceAssistantFeature.VOICE_ASSISTANT + | VoiceAssistantFeature.ANNOUNCE + }, + ) + await hass.async_block_till_done() + + satellite = get_satellite_entity(hass, mock_device.device_info.mac_address) + assert satellite is not None + assert satellite.async_get_configuration().max_active_wake_words == 0 + + # Select should be unavailable + state = hass.states.get("select.test_wake_word") + assert state is not None + assert state.state == STATE_UNAVAILABLE + + +async def test_wake_word_select_no_active_wake_words( + hass: HomeAssistant, + mock_client: APIClient, + mock_esphome_device: Callable[ + [APIClient, list[EntityInfo], list[UserService], list[EntityState]], + Awaitable[MockESPHomeDevice], + ], +) -> None: + """Test wake word select uses first available wake word if none are active.""" + device_config = AssistSatelliteConfiguration( + available_wake_words=[ + AssistSatelliteWakeWord("okay_nabu", "Okay Nabu", ["en"]), + AssistSatelliteWakeWord("hey_jarvis", "Hey Jarvis", ["en"]), + ], + active_wake_words=[], + max_active_wake_words=1, + ) + mock_client.get_voice_assistant_configuration.return_value = device_config + + mock_device: MockESPHomeDevice = await mock_esphome_device( + mock_client=mock_client, + entity_info=[], + user_service=[], + states=[], + device_info={ + "voice_assistant_feature_flags": VoiceAssistantFeature.VOICE_ASSISTANT + | VoiceAssistantFeature.ANNOUNCE + }, + ) + await hass.async_block_till_done() + + satellite = get_satellite_entity(hass, mock_device.device_info.mac_address) + assert satellite is not None + assert not satellite.async_get_configuration().active_wake_words + + # First available wake word should be selected + state = hass.states.get("select.test_wake_word") + assert state is not None + assert state.state == "Okay Nabu" diff --git a/tests/components/esphome/test_update.py b/tests/components/esphome/test_update.py index 910463f6e30..a461f322088 100644 --- a/tests/components/esphome/test_update.py +++ b/tests/components/esphome/test_update.py @@ -119,10 +119,12 @@ async def test_update_entity( # Compile failed, don't try to upload with ( patch( - "esphome_dashboard_api.ESPHomeDashboardAPI.compile", return_value=False + "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.compile", + return_value=False, ) as mock_compile, patch( - "esphome_dashboard_api.ESPHomeDashboardAPI.upload", return_value=True + "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.upload", + return_value=True, ) as mock_upload, pytest.raises( HomeAssistantError, @@ -130,9 +132,9 @@ async def test_update_entity( ), ): await hass.services.async_call( - "update", - "install", - {"entity_id": "update.test_firmware"}, + UPDATE_DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.test_firmware"}, blocking=True, ) @@ -144,10 +146,12 @@ async def test_update_entity( # Compile success, upload fails with ( patch( - "esphome_dashboard_api.ESPHomeDashboardAPI.compile", return_value=True + "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.compile", + return_value=True, ) as mock_compile, patch( - "esphome_dashboard_api.ESPHomeDashboardAPI.upload", return_value=False + "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.upload", + return_value=False, ) as mock_upload, pytest.raises( HomeAssistantError, @@ -155,9 +159,9 @@ async def test_update_entity( ), ): await hass.services.async_call( - "update", - "install", - {"entity_id": "update.test_firmware"}, + UPDATE_DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.test_firmware"}, blocking=True, ) @@ -170,16 +174,18 @@ async def test_update_entity( # Everything works with ( patch( - "esphome_dashboard_api.ESPHomeDashboardAPI.compile", return_value=True + "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.compile", + return_value=True, ) as mock_compile, patch( - "esphome_dashboard_api.ESPHomeDashboardAPI.upload", return_value=True + "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.upload", + return_value=True, ) as mock_upload, ): await hass.services.async_call( - "update", - "install", - {"entity_id": "update.test_firmware"}, + UPDATE_DOMAIN, + SERVICE_INSTALL, + {ATTR_ENTITY_ID: "update.test_firmware"}, blocking=True, ) @@ -286,7 +292,7 @@ async def test_update_entity_dashboard_not_available_startup( """Test ESPHome update entity when dashboard is not available at startup.""" with ( patch( - "esphome_dashboard_api.ESPHomeDashboardAPI.get_devices", + "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.get_devices", side_effect=TimeoutError, ), ): @@ -334,7 +340,7 @@ async def test_update_entity_dashboard_discovered_after_startup_but_update_faile ) -> None: """Test ESPHome update entity when dashboard is discovered after startup and the first update fails.""" with patch( - "esphome_dashboard_api.ESPHomeDashboardAPI.get_devices", + "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.get_devices", side_effect=TimeoutError, ): await async_get_dashboard(hass).async_refresh()