mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 12:17:07 +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_DATE = "HassGetCurrentDate"
|
||||||
INTENT_GET_CURRENT_TIME = "HassGetCurrentTime"
|
INTENT_GET_CURRENT_TIME = "HassGetCurrentTime"
|
||||||
INTENT_RESPOND = "HassRespond"
|
INTENT_RESPOND = "HassRespond"
|
||||||
|
INTENT_BROADCAST = "HassBroadcast"
|
||||||
|
|
||||||
SLOT_SCHEMA = vol.Schema({}, extra=vol.ALLOW_EXTRA)
|
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.config_entries import ConfigEntry, ConfigFlow
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.entity import DeviceInfo
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
from homeassistant.util.ulid import ulid_hex
|
||||||
|
|
||||||
from tests.common import (
|
from tests.common import (
|
||||||
MockConfigEntry,
|
MockConfigEntry,
|
||||||
@ -38,11 +40,17 @@ def mock_tts(mock_tts_cache_dir: pathlib.Path) -> None:
|
|||||||
class MockAssistSatellite(AssistSatelliteEntity):
|
class MockAssistSatellite(AssistSatelliteEntity):
|
||||||
"""Mock Assist Satellite Entity."""
|
"""Mock Assist Satellite Entity."""
|
||||||
|
|
||||||
_attr_name = "Test Entity"
|
def __init__(self, name: str, features: AssistSatelliteEntityFeature) -> None:
|
||||||
_attr_supported_features = AssistSatelliteEntityFeature.ANNOUNCE
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
"""Initialize the mock entity."""
|
"""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.events = []
|
||||||
self.announcements: list[AssistSatelliteAnnouncement] = []
|
self.announcements: list[AssistSatelliteAnnouncement] = []
|
||||||
self.config = AssistSatelliteConfiguration(
|
self.config = AssistSatelliteConfiguration(
|
||||||
@ -83,7 +91,19 @@ class MockAssistSatellite(AssistSatelliteEntity):
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def entity() -> MockAssistSatellite:
|
def entity() -> MockAssistSatellite:
|
||||||
"""Mock Assist Satellite Entity."""
|
"""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
|
@pytest.fixture
|
||||||
@ -99,6 +119,8 @@ async def init_components(
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: ConfigEntry,
|
config_entry: ConfigEntry,
|
||||||
entity: MockAssistSatellite,
|
entity: MockAssistSatellite,
|
||||||
|
entity2: MockAssistSatellite,
|
||||||
|
entity_no_features: MockAssistSatellite,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize components."""
|
"""Initialize components."""
|
||||||
assert await async_setup_component(hass, "homeassistant", {})
|
assert await async_setup_component(hass, "homeassistant", {})
|
||||||
@ -125,7 +147,9 @@ async def init_components(
|
|||||||
async_unload_entry=async_unload_entry_init,
|
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())
|
mock_platform(hass, f"{TEST_DOMAIN}.config_flow", Mock())
|
||||||
|
|
||||||
with mock_config_flow(TEST_DOMAIN, ConfigFlow):
|
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["stt_stream"] is audio_stream
|
||||||
assert kwargs["pipeline_id"] is None
|
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["tts_audio_output"] is None
|
||||||
assert kwargs["wake_word_phrase"] is None
|
assert kwargs["wake_word_phrase"] is None
|
||||||
assert kwargs["audio_settings"] == AudioSettings(
|
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