ESPHome quality improvements round 2 (#143613)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
J. Nick Koston 2025-04-24 09:51:33 -10:00 committed by GitHub
parent 39f3aa7e78
commit 3aa1c60fe3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 288 additions and 292 deletions

View File

@ -109,7 +109,7 @@ def _mired_to_kelvin(mired_temperature: float) -> int:
def _color_mode_to_ha(mode: int) -> str: def _color_mode_to_ha(mode: int) -> str:
"""Convert an esphome color mode to a HA color mode constant. """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 = [] candidates = []
for ha_mode, cap_lists in _COLOR_MODE_MAPPING.items(): for ha_mode, cap_lists in _COLOR_MODE_MAPPING.items():

View File

@ -96,7 +96,7 @@ class EsphomeMediaPlayer(
@property @property
@esphome_float_state_property @esphome_float_state_property
def volume_level(self) -> float | None: def volume_level(self) -> float:
"""Volume level of the media player (0..1).""" """Volume level of the media player (0..1)."""
return self._state.volume return self._state.volume

View File

@ -36,7 +36,7 @@ class EsphomeSwitch(EsphomeEntity[SwitchInfo, SwitchState], SwitchEntity):
@property @property
@esphome_state_property @esphome_state_property
def is_on(self) -> bool | None: def is_on(self) -> bool:
"""Return true if the switch is on.""" """Return true if the switch is on."""
return self._state.state return self._state.state

View File

@ -109,7 +109,6 @@ class ESPHomeDashboardUpdateEntity(
_attr_has_entity_name = True _attr_has_entity_name = True
_attr_device_class = UpdateDeviceClass.FIRMWARE _attr_device_class = UpdateDeviceClass.FIRMWARE
_attr_title = "ESPHome" _attr_title = "ESPHome"
_attr_name = "Firmware"
_attr_release_url = "https://esphome.io/changelog/" _attr_release_url = "https://esphome.io/changelog/"
_attr_entity_registry_enabled_default = False _attr_entity_registry_enabled_default = False
@ -242,7 +241,7 @@ class ESPHomeUpdateEntity(EsphomeEntity[UpdateInfo, UpdateState], UpdateEntity):
@property @property
@esphome_state_property @esphome_state_property
def installed_version(self) -> str | None: def installed_version(self) -> str:
"""Return the installed version.""" """Return the installed version."""
return self._state.current_version return self._state.current_version
@ -260,19 +259,19 @@ class ESPHomeUpdateEntity(EsphomeEntity[UpdateInfo, UpdateState], UpdateEntity):
@property @property
@esphome_state_property @esphome_state_property
def release_summary(self) -> str | None: def release_summary(self) -> str:
"""Return the release summary.""" """Return the release summary."""
return self._state.release_summary return self._state.release_summary
@property @property
@esphome_state_property @esphome_state_property
def release_url(self) -> str | None: def release_url(self) -> str:
"""Return the release URL.""" """Return the release URL."""
return self._state.release_url return self._state.release_url
@property @property
@esphome_state_property @esphome_state_property
def title(self) -> str | None: def title(self) -> str:
"""Return the title of the update.""" """Return the title of the update."""
return self._state.title return self._state.title

View File

@ -65,7 +65,7 @@ class EsphomeValve(EsphomeEntity[ValveInfo, ValveState], ValveEntity):
@property @property
@esphome_state_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 current position of valve. 0 is closed, 100 is open."""
return round(self._state.position * 100.0) return round(self._state.position * 100.0)

View File

@ -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

View File

@ -34,59 +34,28 @@ from homeassistant.components import (
from homeassistant.components.assist_pipeline import PipelineEvent, PipelineEventType from homeassistant.components.assist_pipeline import PipelineEvent, PipelineEventType
from homeassistant.components.assist_satellite import ( from homeassistant.components.assist_satellite import (
AssistSatelliteConfiguration, AssistSatelliteConfiguration,
AssistSatelliteEntity,
AssistSatelliteEntityFeature, AssistSatelliteEntityFeature,
AssistSatelliteWakeWord, AssistSatelliteWakeWord,
) )
# pylint: disable-next=hass-component-root-import # pylint: disable-next=hass-component-root-import
from homeassistant.components.assist_satellite.entity import AssistSatelliteState from homeassistant.components.assist_satellite.entity import AssistSatelliteState
from homeassistant.components.esphome import DOMAIN from homeassistant.components.esphome.assist_satellite import VoiceAssistantUDPServer
from homeassistant.components.esphome.assist_satellite import (
EsphomeAssistSatellite,
VoiceAssistantUDPServer,
)
from homeassistant.components.select import ( from homeassistant.components.select import (
DOMAIN as SELECT_DOMAIN, DOMAIN as SELECT_DOMAIN,
SERVICE_SELECT_OPTION, 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.core import HomeAssistant
from homeassistant.helpers import ( from homeassistant.helpers import device_registry as dr, intent as intent_helper
device_registry as dr,
entity_registry as er,
intent as intent_helper,
)
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.network import get_url from homeassistant.helpers.network import get_url
from .common import get_satellite_entity
from .conftest import MockESPHomeDevice from .conftest import MockESPHomeDevice
from tests.components.tts.common import MockResultStream 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 @pytest.fixture
def mock_wav() -> bytes: def mock_wav() -> bytes:
"""Return test WAV audio.""" """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( async def test_announce_message(
hass: HomeAssistant, hass: HomeAssistant,
mock_client: APIClient, mock_client: APIClient,
@ -1236,7 +1179,7 @@ async def test_announce_message(
assist_satellite.DOMAIN, assist_satellite.DOMAIN,
"announce", "announce",
{ {
"entity_id": satellite.entity_id, ATTR_ENTITY_ID: satellite.entity_id,
"message": "test-text", "message": "test-text",
"preannounce": False, "preannounce": False,
}, },
@ -1326,7 +1269,7 @@ async def test_announce_media_id(
assist_satellite.DOMAIN, assist_satellite.DOMAIN,
"announce", "announce",
{ {
"entity_id": satellite.entity_id, ATTR_ENTITY_ID: satellite.entity_id,
"media_id": "https://www.home-assistant.io/resolved.mp3", "media_id": "https://www.home-assistant.io/resolved.mp3",
"preannounce": False, "preannounce": False,
}, },
@ -1413,7 +1356,7 @@ async def test_announce_message_with_preannounce(
assist_satellite.DOMAIN, assist_satellite.DOMAIN,
"announce", "announce",
{ {
"entity_id": satellite.entity_id, ATTR_ENTITY_ID: satellite.entity_id,
"message": "test-text", "message": "test-text",
"preannounce_media_id": "test-preannounce", "preannounce_media_id": "test-preannounce",
}, },
@ -1423,7 +1366,7 @@ async def test_announce_message_with_preannounce(
assert satellite.state == AssistSatelliteState.IDLE assert satellite.state == AssistSatelliteState.IDLE
async def test_start_conversation_supported_features( async def test_non_default_supported_features(
hass: HomeAssistant, hass: HomeAssistant,
mock_client: APIClient, mock_client: APIClient,
mock_esphome_device: Callable[ mock_esphome_device: Callable[
@ -1431,7 +1374,7 @@ async def test_start_conversation_supported_features(
Awaitable[MockESPHomeDevice], Awaitable[MockESPHomeDevice],
], ],
) -> None: ) -> 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_device: MockESPHomeDevice = await mock_esphome_device(
mock_client=mock_client, mock_client=mock_client,
entity_info=[], entity_info=[],
@ -1449,6 +1392,7 @@ async def test_start_conversation_supported_features(
assert not ( assert not (
satellite.supported_features & AssistSatelliteEntityFeature.START_CONVERSATION satellite.supported_features & AssistSatelliteEntityFeature.START_CONVERSATION
) )
assert not (satellite.supported_features & AssistSatelliteEntityFeature.ANNOUNCE)
async def test_start_conversation_message( async def test_start_conversation_message(
@ -1537,7 +1481,7 @@ async def test_start_conversation_message(
assist_satellite.DOMAIN, assist_satellite.DOMAIN,
"start_conversation", "start_conversation",
{ {
"entity_id": satellite.entity_id, ATTR_ENTITY_ID: satellite.entity_id,
"start_message": "test-text", "start_message": "test-text",
"preannounce": False, "preannounce": False,
}, },
@ -1646,7 +1590,7 @@ async def test_start_conversation_media_id(
assist_satellite.DOMAIN, assist_satellite.DOMAIN,
"start_conversation", "start_conversation",
{ {
"entity_id": satellite.entity_id, ATTR_ENTITY_ID: satellite.entity_id,
"start_media_id": "https://www.home-assistant.io/resolved.mp3", "start_media_id": "https://www.home-assistant.io/resolved.mp3",
"preannounce": False, "preannounce": False,
}, },
@ -1752,7 +1696,7 @@ async def test_start_conversation_message_with_preannounce(
assist_satellite.DOMAIN, assist_satellite.DOMAIN,
"start_conversation", "start_conversation",
{ {
"entity_id": satellite.entity_id, ATTR_ENTITY_ID: satellite.entity_id,
"start_message": "test-text", "start_message": "test-text",
"preannounce_media_id": "test-preannounce", "preannounce_media_id": "test-preannounce",
}, },
@ -1982,7 +1926,7 @@ async def test_wake_word_select(
await hass.services.async_call( await hass.services.async_call(
SELECT_DOMAIN, SELECT_DOMAIN,
SERVICE_SELECT_OPTION, SERVICE_SELECT_OPTION,
{"entity_id": "select.test_wake_word", "option": "Okay Nabu"}, {ATTR_ENTITY_ID: "select.test_wake_word", "option": "Okay Nabu"},
blocking=True, blocking=True,
) )
await hass.async_block_till_done() await hass.async_block_till_done()
@ -1997,122 +1941,3 @@ async def test_wake_word_select(
# Satellite config should have been updated # Satellite config should have been updated
assert satellite.async_get_configuration().active_wake_words == ["okay_nabu"] 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"

View File

@ -72,18 +72,16 @@ async def test_user_connection_works(
) -> None: ) -> None:
"""Test we can finish a config flow.""" """Test we can finish a config flow."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
"esphome", DOMAIN,
context={"source": config_entries.SOURCE_USER}, context={"source": config_entries.SOURCE_USER},
data=None,
) )
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user" assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_configure(
"esphome", result["flow_id"],
context={"source": config_entries.SOURCE_USER}, user_input={CONF_HOST: "127.0.0.1", CONF_PORT: 80},
data={CONF_HOST: "127.0.0.1", CONF_PORT: 80},
) )
assert result["type"] is FlowResultType.CREATE_ENTRY 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) entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
"esphome", DOMAIN,
context={"source": config_entries.SOURCE_USER}, context={"source": config_entries.SOURCE_USER},
data=None, data=None,
) )
@ -127,10 +125,9 @@ async def test_user_connection_updates_host(hass: HomeAssistant) -> None:
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user" assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_configure(
"esphome", result["flow_id"],
context={"source": config_entries.SOURCE_USER}, user_input={CONF_HOST: "127.0.0.1", CONF_PORT: 80},
data={CONF_HOST: "127.0.0.1", CONF_PORT: 80},
) )
assert result["type"] is FlowResultType.ABORT assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured_updates" assert result["reason"] == "already_configured_updates"
@ -157,7 +154,7 @@ async def test_user_sets_unique_id(hass: HomeAssistant) -> None:
type="mock_type", type="mock_type",
) )
discovery_result = await hass.config_entries.flow.async_init( 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 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( result = await hass.config_entries.flow.async_init(
"esphome", DOMAIN,
context={"source": config_entries.SOURCE_USER}, context={"source": config_entries.SOURCE_USER},
data=None, data=None,
) )
@ -211,7 +208,7 @@ async def test_user_resolve_error(hass: HomeAssistant, mock_client: APIClient) -
) as exc: ) as exc:
mock_client.device_info.side_effect = exc mock_client.device_info.side_effect = exc
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
"esphome", DOMAIN,
context={"source": config_entries.SOURCE_USER}, context={"source": config_entries.SOURCE_USER},
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, 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", type="mock_type",
) )
discovery_result = await hass.config_entries.flow.async_init( 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 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( result = await hass.config_entries.flow.async_init(
"esphome", DOMAIN,
context={"source": config_entries.SOURCE_USER}, context={"source": config_entries.SOURCE_USER},
data=None, data=None,
) )
@ -301,7 +298,7 @@ async def test_user_connection_error(
mock_client.device_info.side_effect = APIConnectionError mock_client.device_info.side_effect = APIConnectionError
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
"esphome", DOMAIN,
context={"source": config_entries.SOURCE_USER}, context={"source": config_entries.SOURCE_USER},
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, 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") mock_client.device_info.return_value = DeviceInfo(uses_password=True, name="test")
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
"esphome", DOMAIN,
context={"source": config_entries.SOURCE_USER}, context={"source": config_entries.SOURCE_USER},
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, 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") mock_client.device_info.return_value = DeviceInfo(uses_password=True, name="test")
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
"esphome", DOMAIN,
context={"source": config_entries.SOURCE_USER}, context={"source": config_entries.SOURCE_USER},
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, 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, return_value=WRONG_NOISE_PSK,
): ):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
"esphome", DOMAIN,
context={"source": config_entries.SOURCE_USER}, context={"source": config_entries.SOURCE_USER},
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, 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, return_value=VALID_NOISE_PSK,
): ):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
"esphome", DOMAIN,
context={"source": config_entries.SOURCE_USER}, context={"source": config_entries.SOURCE_USER},
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, 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, side_effect=dashboard_exception,
): ):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
"esphome", DOMAIN,
context={"source": config_entries.SOURCE_USER}, context={"source": config_entries.SOURCE_USER},
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, 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( with patch(
"esphome_dashboard_api.ESPHomeDashboardAPI.get_devices", "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.get_devices",
side_effect=TimeoutError, side_effect=TimeoutError,
): ):
await dashboard.async_get_dashboard(hass).async_refresh() await dashboard.async_get_dashboard(hass).async_refresh()
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
"esphome", DOMAIN,
context={"source": config_entries.SOURCE_USER}, context={"source": config_entries.SOURCE_USER},
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, 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") mock_client.device_info.return_value = DeviceInfo(uses_password=True, name="test")
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
"esphome", DOMAIN,
context={"source": config_entries.SOURCE_USER}, context={"source": config_entries.SOURCE_USER},
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, 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", type="mock_type",
) )
flow = await hass.config_entries.flow.async_init( 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) == { assert get_flow_context(hass, flow) == {
"source": config_entries.SOURCE_ZEROCONF, "source": config_entries.SOURCE_ZEROCONF,
@ -714,7 +711,7 @@ async def test_discovery_no_mac(hass: HomeAssistant) -> None:
type="mock_type", type="mock_type",
) )
flow = await hass.config_entries.flow.async_init( 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["type"] is FlowResultType.ABORT
assert flow["reason"] == "mdns_missing_mac" assert flow["reason"] == "mdns_missing_mac"
@ -741,7 +738,7 @@ async def test_discovery_already_configured(hass: HomeAssistant) -> None:
type="mock_type", type="mock_type",
) )
result = await hass.config_entries.flow.async_init( 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 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( 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["type"] is FlowResultType.FORM
assert result["step_id"] == "discovery_confirm" assert result["step_id"] == "discovery_confirm"
assert result["description_placeholders"] == {"name": "test"} assert result["description_placeholders"] == {"name": "test"}
result = await hass.config_entries.flow.async_init( 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["type"] is FlowResultType.ABORT
assert result["reason"] == "already_in_progress" assert result["reason"] == "already_in_progress"
@ -801,7 +798,7 @@ async def test_discovery_updates_unique_id(hass: HomeAssistant) -> None:
type="mock_type", type="mock_type",
) )
result = await hass.config_entries.flow.async_init( 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 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 mock_client.device_info.side_effect = RequiresEncryptionAPIError
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
"esphome", DOMAIN,
context={"source": config_entries.SOURCE_USER}, context={"source": config_entries.SOURCE_USER},
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, 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 mock_client.device_info.side_effect = RequiresEncryptionAPIError
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
"esphome", DOMAIN,
context={"source": config_entries.SOURCE_USER}, context={"source": config_entries.SOURCE_USER},
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, 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 mock_client.device_info.side_effect = RequiresEncryptionAPIError
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
"esphome", DOMAIN,
context={"source": config_entries.SOURCE_USER}, context={"source": config_entries.SOURCE_USER},
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053},
) )
@ -1301,7 +1298,7 @@ async def test_discovery_dhcp_updates_host(
macaddress="1122334455aa", macaddress="1122334455aa",
) )
result = await hass.config_entries.flow.async_init( 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 assert result["type"] is FlowResultType.ABORT
@ -1337,7 +1334,7 @@ async def test_discovery_dhcp_does_not_update_host_wrong_mac(
macaddress="1122334455aa", macaddress="1122334455aa",
) )
result = await hass.config_entries.flow.async_init( 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 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", macaddress="1122334455aa",
) )
result = await hass.config_entries.flow.async_init( 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 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", macaddress="1122334455aa",
) )
result = await hass.config_entries.flow.async_init( 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 assert result["type"] is FlowResultType.ABORT
@ -1441,7 +1438,7 @@ async def test_discovery_dhcp_no_changes(
macaddress="000000000000", macaddress="000000000000",
) )
result = await hass.config_entries.flow.async_init( 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 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: async def test_discovery_hassio(hass: HomeAssistant) -> None:
"""Test dashboard discovery.""" """Test dashboard discovery."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
"esphome", DOMAIN,
data=HassioServiceInfo( data=HassioServiceInfo(
config={ config={
"host": "mock-esphome", "host": "mock-esphome",
@ -1494,7 +1491,7 @@ async def test_zeroconf_encryption_key_via_dashboard(
type="mock_type", type="mock_type",
) )
flow = await hass.config_entries.flow.async_init( 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 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", type="mock_type",
) )
flow = await hass.config_entries.flow.async_init( 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 assert flow["type"] is FlowResultType.FORM
@ -1625,7 +1622,7 @@ async def test_zeroconf_no_encryption_key_via_dashboard(
type="mock_type", type="mock_type",
) )
flow = await hass.config_entries.flow.async_init( 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 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( result = await hass.config_entries.flow.async_init(
"esphome", DOMAIN,
context={"source": config_entries.SOURCE_USER}, context={"source": config_entries.SOURCE_USER},
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053},
) )
@ -1805,7 +1802,7 @@ async def mqtt_discovery_test_abort(
timestamp=None, timestamp=None,
) )
flow = await hass.config_entries.flow.async_init( 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["type"] is FlowResultType.ABORT
assert flow["reason"] == reason assert flow["reason"] == reason
@ -1849,7 +1846,7 @@ async def test_discovery_mqtt_initiation(hass: HomeAssistant) -> None:
timestamp=None, timestamp=None,
) )
flow = await hass.config_entries.flow.async_init( 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( 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( result = await hass.config_entries.flow.async_init(
"esphome", DOMAIN,
context={"source": config_entries.SOURCE_USER}, context={"source": config_entries.SOURCE_USER},
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, 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( result = await hass.config_entries.flow.async_init(
"esphome", DOMAIN,
context={"source": config_entries.SOURCE_USER}, context={"source": config_entries.SOURCE_USER},
data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053}, data={CONF_HOST: "127.0.0.1", CONF_PORT: 6053},
) )
await hass.async_block_till_done()
assert result["type"] is FlowResultType.MENU assert result["type"] is FlowResultType.MENU
assert result["step_id"] == "name_conflict" assert result["step_id"] == "name_conflict"

View File

@ -6,12 +6,7 @@ from unittest.mock import patch
from aioesphomeapi import DeviceInfo, InvalidAuthAPIError from aioesphomeapi import DeviceInfo, InvalidAuthAPIError
import pytest import pytest
from homeassistant.components.esphome import ( from homeassistant.components.esphome import CONF_NOISE_PSK, DOMAIN, dashboard
CONF_NOISE_PSK,
DOMAIN,
coordinator,
dashboard,
)
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType from homeassistant.data_entry_flow import FlowResultType
@ -117,8 +112,9 @@ async def test_setup_dashboard_fails(
hass_storage: dict[str, Any], hass_storage: dict[str, Any],
) -> None: ) -> None:
"""Test that nothing is stored on failed dashboard setup when there was no dashboard before.""" """Test that nothing is stored on failed dashboard setup when there was no dashboard before."""
with patch.object( with patch(
coordinator.ESPHomeDashboardAPI, "get_devices", side_effect=TimeoutError "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.get_devices",
side_effect=TimeoutError,
) as mock_get_devices: ) as mock_get_devices:
await async_setup_component(hass, DOMAIN, {}) await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done() await hass.async_block_till_done()
@ -136,8 +132,8 @@ async def test_setup_dashboard_fails_when_already_setup(
hass_storage: dict[str, Any], hass_storage: dict[str, Any],
) -> None: ) -> None:
"""Test failed dashboard setup still reloads entries if one existed before.""" """Test failed dashboard setup still reloads entries if one existed before."""
with patch.object( with patch(
coordinator.ESPHomeDashboardAPI, "get_devices" "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.get_devices"
) as mock_get_devices: ) as mock_get_devices:
await dashboard.async_set_dashboard_info( await dashboard.async_set_dashboard_info(
hass, "test-slug", "working-host", 6052 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() await hass.async_block_till_done()
with ( with (
patch.object( patch(
coordinator.ESPHomeDashboardAPI, "get_devices", side_effect=TimeoutError "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.get_devices",
side_effect=TimeoutError,
) as mock_get_devices, ) as mock_get_devices,
patch( patch(
"homeassistant.components.esphome.async_setup_entry", return_value=True "homeassistant.components.esphome.async_setup_entry", return_value=True

View File

@ -221,9 +221,6 @@ async def test_entities_removed_after_reload(
unique_id="my_binary_sensor", unique_id="my_binary_sensor",
), ),
] ]
states = [
BinarySensorState(key=1, state=True, missing_state=False),
]
mock_device.client.list_entities_services = AsyncMock( mock_device.client.list_entities_services = AsyncMock(
return_value=(entity_info, user_service) return_value=(entity_info, user_service)
) )

View File

@ -7,6 +7,8 @@ from aioesphomeapi import (
SensorState, SensorState,
) )
from homeassistant.components.esphome import DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
@ -62,15 +64,15 @@ async def test_migrate_entity_unique_id_downgrade_upgrade(
) -> None: ) -> None:
"""Test unique id migration prefers the original entity on downgrade upgrade.""" """Test unique id migration prefers the original entity on downgrade upgrade."""
entity_registry.async_get_or_create( entity_registry.async_get_or_create(
"sensor", SENSOR_DOMAIN,
"esphome", DOMAIN,
"my_sensor", "my_sensor",
suggested_object_id="old_sensor", suggested_object_id="old_sensor",
disabled_by=None, disabled_by=None,
) )
entity_registry.async_get_or_create( entity_registry.async_get_or_create(
"sensor", SENSOR_DOMAIN,
"esphome", DOMAIN,
"11:22:33:44:55:AA-sensor-mysensor", "11:22:33:44:55:AA-sensor-mysensor",
suggested_object_id="new_sensor", suggested_object_id="new_sensor",
disabled_by=None, 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 # entity that was only created on downgrade and they keep
# the original one. # the original one.
assert ( 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 is not None
) )
# Note that ESPHome includes the EntityInfo type in the unique id # Note that ESPHome includes the EntityInfo type in the unique id

View File

@ -9,9 +9,9 @@ from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@pytest.mark.usefixtures("mock_zeroconf") @pytest.mark.usefixtures("mock_client", "mock_zeroconf")
async def test_delete_entry(hass: HomeAssistant, mock_client) -> None: async def test_delete_entry(hass: HomeAssistant) -> None:
"""Test we can delete an entry with error.""" """Test we can delete an entry without error."""
entry = MockConfigEntry( entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
data={CONF_HOST: "test.local", CONF_PORT: 6053, CONF_PASSWORD: ""}, data={CONF_HOST: "test.local", CONF_PORT: 6053, CONF_PASSWORD: ""},

View File

@ -38,6 +38,8 @@ from homeassistant.components.light import (
from homeassistant.const import ATTR_ENTITY_ID from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
LIGHT_COLOR_CAPABILITY_UNKNOWN = 1 << 8 # 256
async def test_light_on_off( async def test_light_on_off(
hass: HomeAssistant, mock_client: APIClient, mock_generic_device_entry 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, min_mireds=153,
max_mireds=400, max_mireds=400,
supported_color_modes=[ 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, state=True,
color_mode=LightColorCapability.ON_OFF color_mode=LightColorCapability.ON_OFF
| LightColorCapability.BRIGHTNESS | 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, state=True,
color_mode=LightColorCapability.ON_OFF color_mode=LightColorCapability.ON_OFF
| LightColorCapability.BRIGHTNESS | LightColorCapability.BRIGHTNESS
| 1 << 8, | LIGHT_COLOR_CAPABILITY_UNKNOWN,
brightness=pytest.approx(0.4980392156862745), brightness=pytest.approx(0.4980392156862745),
) )
] ]

View File

@ -34,6 +34,7 @@ from homeassistant.components.esphome.const import (
STABLE_BLE_VERSION_STR, STABLE_BLE_VERSION_STR,
) )
from homeassistant.components.esphome.manager import DEVICE_CONFLICT_ISSUE_FORMAT from homeassistant.components.esphome.manager import DEVICE_CONFLICT_ISSUE_FORMAT
from homeassistant.components.tag import DOMAIN as TAG_DOMAIN
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_HOST,
CONF_PASSWORD, CONF_PASSWORD,
@ -192,7 +193,7 @@ async def test_esphome_device_service_calls_allowed(
issue_registry: ir.IssueRegistry, issue_registry: ir.IssueRegistry,
) -> None: ) -> None:
"""Test a device with service calls are allowed.""" """Test a device with service calls are allowed."""
await async_setup_component(hass, "tag", {}) await async_setup_component(hass, TAG_DOMAIN, {})
entity_info = [] entity_info = []
states = [] states = []
user_service = [] user_service = []

View File

@ -1,9 +1,22 @@
"""Test ESPHome selects.""" """Test ESPHome selects."""
from collections.abc import Awaitable, Callable
from unittest.mock import call 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 ( from homeassistant.components.select import (
ATTR_OPTION, ATTR_OPTION,
DOMAIN as SELECT_DOMAIN, DOMAIN as SELECT_DOMAIN,
@ -12,6 +25,9 @@ from homeassistant.components.select import (
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .common import get_satellite_entity
from .conftest import MockESPHomeDevice
async def test_pipeline_selector( async def test_pipeline_selector(
hass: HomeAssistant, hass: HomeAssistant,
@ -80,3 +96,122 @@ async def test_select_generic_entity(
blocking=True, blocking=True,
) )
mock_client.select_command.assert_has_calls([call(1, "b")]) 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"

View File

@ -119,10 +119,12 @@ async def test_update_entity(
# Compile failed, don't try to upload # Compile failed, don't try to upload
with ( with (
patch( patch(
"esphome_dashboard_api.ESPHomeDashboardAPI.compile", return_value=False "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.compile",
return_value=False,
) as mock_compile, ) as mock_compile,
patch( patch(
"esphome_dashboard_api.ESPHomeDashboardAPI.upload", return_value=True "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.upload",
return_value=True,
) as mock_upload, ) as mock_upload,
pytest.raises( pytest.raises(
HomeAssistantError, HomeAssistantError,
@ -130,9 +132,9 @@ async def test_update_entity(
), ),
): ):
await hass.services.async_call( await hass.services.async_call(
"update", UPDATE_DOMAIN,
"install", SERVICE_INSTALL,
{"entity_id": "update.test_firmware"}, {ATTR_ENTITY_ID: "update.test_firmware"},
blocking=True, blocking=True,
) )
@ -144,10 +146,12 @@ async def test_update_entity(
# Compile success, upload fails # Compile success, upload fails
with ( with (
patch( patch(
"esphome_dashboard_api.ESPHomeDashboardAPI.compile", return_value=True "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.compile",
return_value=True,
) as mock_compile, ) as mock_compile,
patch( patch(
"esphome_dashboard_api.ESPHomeDashboardAPI.upload", return_value=False "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.upload",
return_value=False,
) as mock_upload, ) as mock_upload,
pytest.raises( pytest.raises(
HomeAssistantError, HomeAssistantError,
@ -155,9 +159,9 @@ async def test_update_entity(
), ),
): ):
await hass.services.async_call( await hass.services.async_call(
"update", UPDATE_DOMAIN,
"install", SERVICE_INSTALL,
{"entity_id": "update.test_firmware"}, {ATTR_ENTITY_ID: "update.test_firmware"},
blocking=True, blocking=True,
) )
@ -170,16 +174,18 @@ async def test_update_entity(
# Everything works # Everything works
with ( with (
patch( patch(
"esphome_dashboard_api.ESPHomeDashboardAPI.compile", return_value=True "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.compile",
return_value=True,
) as mock_compile, ) as mock_compile,
patch( patch(
"esphome_dashboard_api.ESPHomeDashboardAPI.upload", return_value=True "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.upload",
return_value=True,
) as mock_upload, ) as mock_upload,
): ):
await hass.services.async_call( await hass.services.async_call(
"update", UPDATE_DOMAIN,
"install", SERVICE_INSTALL,
{"entity_id": "update.test_firmware"}, {ATTR_ENTITY_ID: "update.test_firmware"},
blocking=True, 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.""" """Test ESPHome update entity when dashboard is not available at startup."""
with ( with (
patch( patch(
"esphome_dashboard_api.ESPHomeDashboardAPI.get_devices", "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.get_devices",
side_effect=TimeoutError, side_effect=TimeoutError,
), ),
): ):
@ -334,7 +340,7 @@ async def test_update_entity_dashboard_discovered_after_startup_but_update_faile
) -> None: ) -> None:
"""Test ESPHome update entity when dashboard is discovered after startup and the first update fails.""" """Test ESPHome update entity when dashboard is discovered after startup and the first update fails."""
with patch( with patch(
"esphome_dashboard_api.ESPHomeDashboardAPI.get_devices", "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.get_devices",
side_effect=TimeoutError, side_effect=TimeoutError,
): ):
await async_get_dashboard(hass).async_refresh() await async_get_dashboard(hass).async_refresh()