mirror of
https://github.com/home-assistant/core.git
synced 2026-02-14 23:28:42 +00:00
feat: add info skills to alexa devices (#162097)
This commit is contained in:
@@ -29,3 +29,24 @@ COUNTRY_DOMAINS = {
|
||||
|
||||
CATEGORY_SENSORS = "sensors"
|
||||
CATEGORY_NOTIFICATIONS = "notifications"
|
||||
|
||||
# Map service translation keys to Alexa API
|
||||
INFO_SKILLS_MAPPING = {
|
||||
"calendar_today": "Alexa.Calendar.PlayToday",
|
||||
"calendar_tomorrow": "Alexa.Calendar.PlayTomorrow",
|
||||
"calendar_next": "Alexa.Calendar.PlayNext",
|
||||
"date": "Alexa.Date.Play",
|
||||
"time": "Alexa.Time.Play",
|
||||
"national_news": "Alexa.News.NationalNews",
|
||||
"flash_briefing": "Alexa.FlashBriefing.Play",
|
||||
"traffic": "Alexa.Traffic.Play",
|
||||
"weather": "Alexa.Weather.Play",
|
||||
"cleanup": "Alexa.CleanUp.Play",
|
||||
"good_morning": "Alexa.GoodMorning.Play",
|
||||
"sing_song": "Alexa.SingASong.Play",
|
||||
"fun_fact": "Alexa.FunFact.Play",
|
||||
"tell_joke": "Alexa.Joke.Play",
|
||||
"tell_story": "Alexa.TellStory.Play",
|
||||
"im_home": "Alexa.ImHome.Play",
|
||||
"goodnight": "Alexa.GoodNight.Play",
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"services": {
|
||||
"send_info_skill": {
|
||||
"service": "mdi:information"
|
||||
},
|
||||
"send_sound": {
|
||||
"service": "mdi:cast-audio"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Support for services."""
|
||||
|
||||
from aioamazondevices.const.metadata import ALEXA_INFO_SKILLS
|
||||
from aioamazondevices.const.sounds import SOUNDS_LIST
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -9,13 +10,15 @@ from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import DOMAIN, INFO_SKILLS_MAPPING
|
||||
from .coordinator import AmazonConfigEntry
|
||||
|
||||
ATTR_TEXT_COMMAND = "text_command"
|
||||
ATTR_SOUND = "sound"
|
||||
ATTR_INFO_SKILL = "info_skill"
|
||||
SERVICE_TEXT_COMMAND = "send_text_command"
|
||||
SERVICE_SOUND_NOTIFICATION = "send_sound"
|
||||
SERVICE_INFO_SKILL = "send_info_skill"
|
||||
|
||||
SCHEMA_SOUND_SERVICE = vol.Schema(
|
||||
{
|
||||
@@ -29,6 +32,12 @@ SCHEMA_CUSTOM_COMMAND = vol.Schema(
|
||||
vol.Required(ATTR_DEVICE_ID): cv.string,
|
||||
}
|
||||
)
|
||||
SCHEMA_INFO_SKILL = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_INFO_SKILL): cv.string,
|
||||
vol.Required(ATTR_DEVICE_ID): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
@@ -86,6 +95,17 @@ async def _async_execute_action(call: ServiceCall, attribute: str) -> None:
|
||||
await coordinator.api.call_alexa_text_command(
|
||||
coordinator.data[device.serial_number], value
|
||||
)
|
||||
elif attribute == ATTR_INFO_SKILL:
|
||||
info_skill = INFO_SKILLS_MAPPING.get(value)
|
||||
if info_skill not in ALEXA_INFO_SKILLS:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_info_skill_value",
|
||||
translation_placeholders={"info_skill": value},
|
||||
)
|
||||
await coordinator.api.call_alexa_info_skill(
|
||||
coordinator.data[device.serial_number], value
|
||||
)
|
||||
|
||||
|
||||
async def async_send_sound_notification(call: ServiceCall) -> None:
|
||||
@@ -98,6 +118,11 @@ async def async_send_text_command(call: ServiceCall) -> None:
|
||||
await _async_execute_action(call, ATTR_TEXT_COMMAND)
|
||||
|
||||
|
||||
async def async_send_info_skill(call: ServiceCall) -> None:
|
||||
"""Send an info skill command to a AmazonDevice."""
|
||||
await _async_execute_action(call, ATTR_INFO_SKILL)
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up the services for the Amazon Devices integration."""
|
||||
@@ -112,5 +137,10 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
async_send_text_command,
|
||||
SCHEMA_CUSTOM_COMMAND,
|
||||
),
|
||||
(
|
||||
SERVICE_INFO_SKILL,
|
||||
async_send_info_skill,
|
||||
SCHEMA_INFO_SKILL,
|
||||
),
|
||||
):
|
||||
hass.services.async_register(DOMAIN, service_name, method, schema=schema)
|
||||
|
||||
@@ -67,3 +67,36 @@ send_sound:
|
||||
- squeaky_12
|
||||
- zap_01
|
||||
translation_key: sound
|
||||
|
||||
send_info_skill:
|
||||
fields:
|
||||
device_id:
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: alexa_devices
|
||||
info_skill:
|
||||
required: true
|
||||
example: date
|
||||
default: date
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- calendar_today
|
||||
- calendar_tomorrow
|
||||
- calendar_next
|
||||
- date
|
||||
- time
|
||||
- national_news
|
||||
- flash_briefing
|
||||
- traffic
|
||||
- weather
|
||||
- cleanup
|
||||
- good_morning
|
||||
- sing_song
|
||||
- fun_fact
|
||||
- tell_joke
|
||||
- tell_story
|
||||
- im_home
|
||||
- goodnight
|
||||
translation_key: info_skill
|
||||
|
||||
@@ -102,11 +102,35 @@
|
||||
"invalid_device_id": {
|
||||
"message": "Invalid device ID specified: {device_id}"
|
||||
},
|
||||
"invalid_info_skill_value": {
|
||||
"message": "Invalid info skill {info_skill} specified"
|
||||
},
|
||||
"invalid_sound_value": {
|
||||
"message": "Invalid sound {sound} specified"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"info_skill": {
|
||||
"options": {
|
||||
"calendar_next": "Calendar: Next event",
|
||||
"calendar_today": "Calendar: Today's Calendar",
|
||||
"calendar_tomorrow": "Calendar: Tomorrow's Calendar",
|
||||
"cleanup": "Encourage me to clean up",
|
||||
"date": "Date",
|
||||
"flash_briefing": "Flash Briefing",
|
||||
"fun_fact": "Tell me a fun fact",
|
||||
"good_morning": "Good morning",
|
||||
"goodnight": "Wish me a good night",
|
||||
"im_home": "Welcome me home",
|
||||
"national_news": "National News",
|
||||
"sing_song": "Sing a song",
|
||||
"tell_joke": "Tell me a joke",
|
||||
"tell_story": "Tell me a story",
|
||||
"time": "Time",
|
||||
"traffic": "Traffic",
|
||||
"weather": "Weather"
|
||||
}
|
||||
},
|
||||
"sound": {
|
||||
"options": {
|
||||
"air_horn_03": "Air horn",
|
||||
@@ -154,6 +178,20 @@
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"send_info_skill": {
|
||||
"description": "Sends an info skill command to a device",
|
||||
"fields": {
|
||||
"device_id": {
|
||||
"description": "[%key:component::alexa_devices::common::device_id_description%]",
|
||||
"name": "Device"
|
||||
},
|
||||
"info_skill": {
|
||||
"description": "The info skill command to send.",
|
||||
"name": "Alexa info skill command"
|
||||
}
|
||||
},
|
||||
"name": "Send info skill command"
|
||||
},
|
||||
"send_sound": {
|
||||
"description": "Sends a sound to a device",
|
||||
"fields": {
|
||||
|
||||
@@ -1,4 +1,71 @@
|
||||
# serializer version: 1
|
||||
# name: test_info_skill_service
|
||||
_Call(
|
||||
tuple(
|
||||
dict({
|
||||
'account_name': 'Echo Test',
|
||||
'capabilities': list([
|
||||
'AUDIO_PLAYER',
|
||||
'MICROPHONE',
|
||||
]),
|
||||
'device_cluster_members': list([
|
||||
'echo_test_serial_number',
|
||||
]),
|
||||
'device_family': 'mine',
|
||||
'device_owner_customer_id': 'amazon_ower_id',
|
||||
'device_type': 'echo',
|
||||
'endpoint_id': 'G1234567890123456789012345678A',
|
||||
'entity_id': '11111111-2222-3333-4444-555555555555',
|
||||
'household_device': False,
|
||||
'notifications': dict({
|
||||
'Alarm': dict({
|
||||
'label': 'Morning Alarm',
|
||||
'next_occurrence': datetime.datetime(2023, 10, 1, 7, 0, tzinfo=datetime.timezone.utc),
|
||||
'status': 'ON',
|
||||
'type': 'Alarm',
|
||||
}),
|
||||
'Reminder': dict({
|
||||
'label': 'Take out the trash',
|
||||
'next_occurrence': None,
|
||||
'status': 'ON',
|
||||
'type': 'Reminder',
|
||||
}),
|
||||
'Timer': dict({
|
||||
'label': '',
|
||||
'next_occurrence': None,
|
||||
'status': 'OFF',
|
||||
'type': 'Timer',
|
||||
}),
|
||||
}),
|
||||
'notifications_supported': True,
|
||||
'online': True,
|
||||
'sensors': dict({
|
||||
'dnd': dict({
|
||||
'error': False,
|
||||
'error_msg': None,
|
||||
'error_type': None,
|
||||
'name': 'dnd',
|
||||
'scale': None,
|
||||
'value': False,
|
||||
}),
|
||||
'temperature': dict({
|
||||
'error': False,
|
||||
'error_msg': None,
|
||||
'error_type': None,
|
||||
'name': 'temperature',
|
||||
'scale': 'CELSIUS',
|
||||
'value': '22.5',
|
||||
}),
|
||||
}),
|
||||
'serial_number': 'echo_test_serial_number',
|
||||
'software_version': 'echo_test_software_version',
|
||||
}),
|
||||
'tell_joke',
|
||||
),
|
||||
dict({
|
||||
}),
|
||||
)
|
||||
# ---
|
||||
# name: test_send_sound_service
|
||||
_Call(
|
||||
tuple(
|
||||
|
||||
@@ -7,8 +7,10 @@ from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.alexa_devices.const import DOMAIN
|
||||
from homeassistant.components.alexa_devices.services import (
|
||||
ATTR_INFO_SKILL,
|
||||
ATTR_SOUND,
|
||||
ATTR_TEXT_COMMAND,
|
||||
SERVICE_INFO_SKILL,
|
||||
SERVICE_SOUND_NOTIFICATION,
|
||||
SERVICE_TEXT_COMMAND,
|
||||
)
|
||||
@@ -35,6 +37,37 @@ async def test_setup_services(
|
||||
assert (services := hass.services.async_services_for_domain(DOMAIN))
|
||||
assert SERVICE_TEXT_COMMAND in services
|
||||
assert SERVICE_SOUND_NOTIFICATION in services
|
||||
assert SERVICE_INFO_SKILL in services
|
||||
|
||||
|
||||
async def test_info_skill_service(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
mock_amazon_devices_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test info skill service."""
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
device_entry = device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, TEST_DEVICE_1_SN)}
|
||||
)
|
||||
assert device_entry
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_INFO_SKILL,
|
||||
{
|
||||
ATTR_INFO_SKILL: "tell_joke",
|
||||
ATTR_DEVICE_ID: device_entry.id,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_amazon_devices_client.call_alexa_info_skill.call_count == 1
|
||||
assert mock_amazon_devices_client.call_alexa_info_skill.call_args == snapshot
|
||||
|
||||
|
||||
async def test_send_sound_service(
|
||||
@@ -153,6 +186,62 @@ async def test_invalid_parameters(
|
||||
assert exc_info.value.translation_placeholders == translation_placeholders
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("info_skill", "device_id", "translation_key", "translation_placeholders"),
|
||||
[
|
||||
(
|
||||
"tell_joke",
|
||||
"fake_device_id",
|
||||
"invalid_device_id",
|
||||
{"device_id": "fake_device_id"},
|
||||
),
|
||||
(
|
||||
"wrong_info_skill_name",
|
||||
TEST_DEVICE_1_ID,
|
||||
"invalid_info_skill_value",
|
||||
{
|
||||
"info_skill": "wrong_info_skill_name",
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_invalid_info_skillparameters(
|
||||
hass: HomeAssistant,
|
||||
mock_amazon_devices_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
info_skill: str,
|
||||
device_id: str,
|
||||
translation_key: str,
|
||||
translation_placeholders: dict[str, str],
|
||||
) -> None:
|
||||
"""Test invalid info skill service parameters."""
|
||||
|
||||
device_entry = dr.DeviceEntry(
|
||||
id=TEST_DEVICE_1_ID, identifiers={(DOMAIN, TEST_DEVICE_1_SN)}
|
||||
)
|
||||
mock_device_registry(
|
||||
hass,
|
||||
{device_entry.id: device_entry},
|
||||
)
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
# Call Service
|
||||
with pytest.raises(ServiceValidationError) as exc_info:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_INFO_SKILL,
|
||||
{
|
||||
ATTR_INFO_SKILL: info_skill,
|
||||
ATTR_DEVICE_ID: device_id,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert exc_info.value.translation_domain == DOMAIN
|
||||
assert exc_info.value.translation_key == translation_key
|
||||
assert exc_info.value.translation_placeholders == translation_placeholders
|
||||
|
||||
|
||||
async def test_config_entry_not_loaded(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
|
||||
Reference in New Issue
Block a user