mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 01:08:12 +00:00
Add broadcast intent (#135337)
This commit is contained in:
parent
6e255060c6
commit
762bc7b8d1
69
homeassistant/components/assist_satellite/intent.py
Normal file
69
homeassistant/components/assist_satellite/intent.py
Normal file
@ -0,0 +1,69 @@
|
||||
"""Assist Satellite intents."""
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er, intent
|
||||
|
||||
from .const import DOMAIN, AssistSatelliteEntityFeature
|
||||
|
||||
|
||||
async def async_setup_intents(hass: HomeAssistant) -> None:
|
||||
"""Set up the intents."""
|
||||
intent.async_register(hass, BroadcastIntentHandler())
|
||||
|
||||
|
||||
class BroadcastIntentHandler(intent.IntentHandler):
|
||||
"""Broadcast a message."""
|
||||
|
||||
intent_type = intent.INTENT_BROADCAST
|
||||
description = "Broadcast a message through the home"
|
||||
|
||||
@property
|
||||
def slot_schema(self) -> dict | None:
|
||||
"""Return a slot schema."""
|
||||
return {vol.Required("message"): str}
|
||||
|
||||
async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse:
|
||||
"""Broadcast a message."""
|
||||
hass = intent_obj.hass
|
||||
ent_reg = er.async_get(hass)
|
||||
|
||||
# Find all assist satellite entities that are not the one invoking the intent
|
||||
entities = {
|
||||
entity: entry
|
||||
for entity in hass.states.async_entity_ids(DOMAIN)
|
||||
if (entry := ent_reg.async_get(entity))
|
||||
and entry.supported_features & AssistSatelliteEntityFeature.ANNOUNCE
|
||||
}
|
||||
|
||||
if intent_obj.device_id:
|
||||
entities = {
|
||||
entity: entry
|
||||
for entity, entry in entities.items()
|
||||
if entry.device_id != intent_obj.device_id
|
||||
}
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
"announce",
|
||||
{"message": intent_obj.slots["message"]["value"]},
|
||||
blocking=True,
|
||||
context=intent_obj.context,
|
||||
target={"entity_id": list(entities)},
|
||||
)
|
||||
|
||||
response = intent_obj.create_response()
|
||||
response.async_set_speech("Done")
|
||||
response.response_type = intent.IntentResponseType.ACTION_DONE
|
||||
response.async_set_results(
|
||||
success_results=[
|
||||
intent.IntentResponseTarget(
|
||||
type=intent.IntentResponseTargetType.ENTITY,
|
||||
id=entity,
|
||||
name=state.name if (state := hass.states.get(entity)) else entity,
|
||||
)
|
||||
for entity in entities
|
||||
]
|
||||
)
|
||||
return response
|
@ -58,6 +58,7 @@ INTENT_TIMER_STATUS = "HassTimerStatus"
|
||||
INTENT_GET_CURRENT_DATE = "HassGetCurrentDate"
|
||||
INTENT_GET_CURRENT_TIME = "HassGetCurrentTime"
|
||||
INTENT_RESPOND = "HassRespond"
|
||||
INTENT_BROADCAST = "HassBroadcast"
|
||||
|
||||
SLOT_SCHEMA = vol.Schema({}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
@ -16,7 +16,9 @@ from homeassistant.components.assist_satellite import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util.ulid import ulid_hex
|
||||
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
@ -38,11 +40,17 @@ def mock_tts(mock_tts_cache_dir: pathlib.Path) -> None:
|
||||
class MockAssistSatellite(AssistSatelliteEntity):
|
||||
"""Mock Assist Satellite Entity."""
|
||||
|
||||
_attr_name = "Test Entity"
|
||||
_attr_supported_features = AssistSatelliteEntityFeature.ANNOUNCE
|
||||
|
||||
def __init__(self) -> None:
|
||||
def __init__(self, name: str, features: AssistSatelliteEntityFeature) -> None:
|
||||
"""Initialize the mock entity."""
|
||||
self._attr_unique_id = ulid_hex()
|
||||
self._attr_device_info = DeviceInfo(
|
||||
{
|
||||
"name": name,
|
||||
"identifiers": {(TEST_DOMAIN, self._attr_unique_id)},
|
||||
}
|
||||
)
|
||||
self._attr_name = name
|
||||
self._attr_supported_features = features
|
||||
self.events = []
|
||||
self.announcements: list[AssistSatelliteAnnouncement] = []
|
||||
self.config = AssistSatelliteConfiguration(
|
||||
@ -83,7 +91,19 @@ class MockAssistSatellite(AssistSatelliteEntity):
|
||||
@pytest.fixture
|
||||
def entity() -> MockAssistSatellite:
|
||||
"""Mock Assist Satellite Entity."""
|
||||
return MockAssistSatellite()
|
||||
return MockAssistSatellite("Test Entity", AssistSatelliteEntityFeature.ANNOUNCE)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def entity2() -> MockAssistSatellite:
|
||||
"""Mock a second Assist Satellite Entity."""
|
||||
return MockAssistSatellite("Test Entity 2", AssistSatelliteEntityFeature.ANNOUNCE)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def entity_no_features() -> MockAssistSatellite:
|
||||
"""Mock a third Assist Satellite Entity."""
|
||||
return MockAssistSatellite("Test Entity No features", 0)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -99,6 +119,8 @@ async def init_components(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
entity: MockAssistSatellite,
|
||||
entity2: MockAssistSatellite,
|
||||
entity_no_features: MockAssistSatellite,
|
||||
) -> None:
|
||||
"""Initialize components."""
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
@ -125,7 +147,9 @@ async def init_components(
|
||||
async_unload_entry=async_unload_entry_init,
|
||||
),
|
||||
)
|
||||
setup_test_component_platform(hass, AS_DOMAIN, [entity], from_config_entry=True)
|
||||
setup_test_component_platform(
|
||||
hass, AS_DOMAIN, [entity, entity2, entity_no_features], from_config_entry=True
|
||||
)
|
||||
mock_platform(hass, f"{TEST_DOMAIN}.config_flow", Mock())
|
||||
|
||||
with mock_config_flow(TEST_DOMAIN, ConfigFlow):
|
||||
|
@ -63,7 +63,7 @@ async def test_entity_state(
|
||||
)
|
||||
assert kwargs["stt_stream"] is audio_stream
|
||||
assert kwargs["pipeline_id"] is None
|
||||
assert kwargs["device_id"] is None
|
||||
assert kwargs["device_id"] is entity.device_entry.id
|
||||
assert kwargs["tts_audio_output"] is None
|
||||
assert kwargs["wake_word_phrase"] is None
|
||||
assert kwargs["audio_settings"] == AudioSettings(
|
||||
|
110
tests/components/assist_satellite/test_intent.py
Normal file
110
tests/components/assist_satellite/test_intent.py
Normal file
@ -0,0 +1,110 @@
|
||||
"""Test assist satellite intents."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.media_source import PlayMedia
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import intent
|
||||
|
||||
from .conftest import MockAssistSatellite
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_tts():
|
||||
"""Mock TTS service."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.assist_satellite.entity.tts_generate_media_source_id",
|
||||
return_value="media-source://bla",
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.media_source.async_resolve_media",
|
||||
return_value=PlayMedia(
|
||||
url="https://www.home-assistant.io/resolved.mp3",
|
||||
mime_type="audio/mp3",
|
||||
),
|
||||
),
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
async def test_broadcast_intent(
|
||||
hass: HomeAssistant,
|
||||
init_components: ConfigEntry,
|
||||
entity: MockAssistSatellite,
|
||||
entity2: MockAssistSatellite,
|
||||
entity_no_features: MockAssistSatellite,
|
||||
mock_tts: None,
|
||||
) -> None:
|
||||
"""Test we can invoke a broadcast intent."""
|
||||
|
||||
result = await intent.async_handle(
|
||||
hass, "test", intent.INTENT_BROADCAST, {"message": {"value": "Hello"}}
|
||||
)
|
||||
|
||||
assert result.as_dict() == {
|
||||
"card": {},
|
||||
"data": {
|
||||
"failed": [],
|
||||
"success": [
|
||||
{
|
||||
"id": "assist_satellite.test_entity",
|
||||
"name": "Test Entity",
|
||||
"type": intent.IntentResponseTargetType.ENTITY,
|
||||
},
|
||||
{
|
||||
"id": "assist_satellite.test_entity_2",
|
||||
"name": "Test Entity 2",
|
||||
"type": intent.IntentResponseTargetType.ENTITY,
|
||||
},
|
||||
],
|
||||
"targets": [],
|
||||
},
|
||||
"language": "en",
|
||||
"response_type": "action_done",
|
||||
"speech": {
|
||||
"plain": {
|
||||
"extra_data": None,
|
||||
"speech": "Done",
|
||||
}
|
||||
},
|
||||
}
|
||||
assert len(entity.announcements) == 1
|
||||
assert len(entity2.announcements) == 1
|
||||
assert len(entity_no_features.announcements) == 0
|
||||
|
||||
result = await intent.async_handle(
|
||||
hass,
|
||||
"test",
|
||||
intent.INTENT_BROADCAST,
|
||||
{"message": {"value": "Hello"}},
|
||||
device_id=entity.device_entry.id,
|
||||
)
|
||||
# Broadcast doesn't targets device that triggered it.
|
||||
assert result.as_dict() == {
|
||||
"card": {},
|
||||
"data": {
|
||||
"failed": [],
|
||||
"success": [
|
||||
{
|
||||
"id": "assist_satellite.test_entity_2",
|
||||
"name": "Test Entity 2",
|
||||
"type": intent.IntentResponseTargetType.ENTITY,
|
||||
},
|
||||
],
|
||||
"targets": [],
|
||||
},
|
||||
"language": "en",
|
||||
"response_type": "action_done",
|
||||
"speech": {
|
||||
"plain": {
|
||||
"extra_data": None,
|
||||
"speech": "Done",
|
||||
}
|
||||
},
|
||||
}
|
||||
assert len(entity.announcements) == 1
|
||||
assert len(entity2.announcements) == 2
|
Loading…
x
Reference in New Issue
Block a user