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:
"""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():

View File

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

View File

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

View File

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

View File

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

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_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"

View File

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

View File

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

View File

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

View File

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

View File

@ -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: ""},

View File

@ -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),
)
]

View File

@ -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 = []

View File

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

View File

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