mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 17:57:55 +00:00
Add alexa remote support (#120878)
* Updated the AlexaModeController to support the remote domain. Also added an alexa entitiy adapter for the remote domain. * Fixed copy paste mistake. * Fixed power state for remove domain. * Updated the CapabilityResource to support labels with the corresponding locale. This local is read from the users config. * Add the alexa display category 'REMOTE' and use it for the remote capability. * Revert "Updated the CapabilityResource to support labels with the corresponding locale. This local is read from the users config." This reverts commit fbdf37904a3e613f4d3107a1b176fdfe6c9429bf. * Fix error when the remote does not have an activtiy list. * Add tests for the state report of a remote entity. * Add a test for alexas set mode directive for a remote entitiy. * Add a test for alexas TurnOn and TurnOff directives for a remote entity. * Apply suggestions from code review Fix copy paste mistakes. Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com> * Improve attribute name as suggested. Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com> * Add test case with zero and one activity. * Add a comment why we use the mode controller instead of the input controller. * Add test to check of the discovery returns all required interfaces for a remote entitiy. * Tweak comment * Add line breaks to fix max allowed chars per line. --------- Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
This commit is contained in:
parent
154da1b18b
commit
e44f00cf7c
@ -18,6 +18,7 @@ from homeassistant.components import (
|
||||
light,
|
||||
media_player,
|
||||
number,
|
||||
remote,
|
||||
timer,
|
||||
vacuum,
|
||||
valve,
|
||||
@ -438,6 +439,8 @@ class AlexaPowerController(AlexaCapability):
|
||||
is_on = self.entity.state == fan.STATE_ON
|
||||
elif self.entity.domain == humidifier.DOMAIN:
|
||||
is_on = self.entity.state == humidifier.STATE_ON
|
||||
elif self.entity.domain == remote.DOMAIN:
|
||||
is_on = self.entity.state not in (STATE_OFF, STATE_UNKNOWN)
|
||||
elif self.entity.domain == vacuum.DOMAIN:
|
||||
is_on = self.entity.state == vacuum.STATE_CLEANING
|
||||
elif self.entity.domain == timer.DOMAIN:
|
||||
@ -1435,6 +1438,12 @@ class AlexaModeController(AlexaCapability):
|
||||
if mode in modes:
|
||||
return f"{humidifier.ATTR_MODE}.{mode}"
|
||||
|
||||
# Remote Activity
|
||||
if self.instance == f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}":
|
||||
activity = self.entity.attributes.get(remote.ATTR_CURRENT_ACTIVITY, None)
|
||||
if activity in self.entity.attributes.get(remote.ATTR_ACTIVITY_LIST, []):
|
||||
return f"{remote.ATTR_ACTIVITY}.{activity}"
|
||||
|
||||
# Water heater operation mode
|
||||
if self.instance == f"{water_heater.DOMAIN}.{water_heater.ATTR_OPERATION_MODE}":
|
||||
operation_mode = self.entity.attributes.get(
|
||||
@ -1549,6 +1558,24 @@ class AlexaModeController(AlexaCapability):
|
||||
)
|
||||
return self._resource.serialize_capability_resources()
|
||||
|
||||
# Remote Resource
|
||||
if self.instance == f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}":
|
||||
# Use the mode controller for a remote because the input controller
|
||||
# only allows a preset of names as an input.
|
||||
self._resource = AlexaModeResource([AlexaGlobalCatalog.SETTING_MODE], False)
|
||||
activities = self.entity.attributes.get(remote.ATTR_ACTIVITY_LIST) or []
|
||||
for activity in activities:
|
||||
self._resource.add_mode(
|
||||
f"{remote.ATTR_ACTIVITY}.{activity}", [activity]
|
||||
)
|
||||
# Remotes with a single activity completely break Alexa discovery, add a
|
||||
# fake activity to the mode controller (see issue #53832).
|
||||
if len(activities) == 1:
|
||||
self._resource.add_mode(
|
||||
f"{remote.ATTR_ACTIVITY}.{PRESET_MODE_NA}", [PRESET_MODE_NA]
|
||||
)
|
||||
return self._resource.serialize_capability_resources()
|
||||
|
||||
# Cover Position Resources
|
||||
if self.instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||
self._resource = AlexaModeResource(
|
||||
|
@ -88,7 +88,7 @@ API_THERMOSTAT_MODES_CUSTOM = {
|
||||
API_THERMOSTAT_PRESETS = {climate.PRESET_ECO: "ECO"}
|
||||
|
||||
# AlexaModeController does not like a single mode for the fan preset or humidifier mode,
|
||||
# we add PRESET_MODE_NA if a fan / humidifier has only one preset_mode
|
||||
# we add PRESET_MODE_NA if a fan / humidifier / remote has only one preset_mode
|
||||
PRESET_MODE_NA = "-"
|
||||
|
||||
STORAGE_ACCESS_TOKEN = "access_token"
|
||||
|
@ -27,6 +27,7 @@ from homeassistant.components import (
|
||||
lock,
|
||||
media_player,
|
||||
number,
|
||||
remote,
|
||||
scene,
|
||||
script,
|
||||
sensor,
|
||||
@ -196,6 +197,10 @@ class DisplayCategory:
|
||||
# Indicates a device that prints.
|
||||
PRINTER = "PRINTER"
|
||||
|
||||
# Indicates a decive that support stateless events,
|
||||
# such as remote switches and smart buttons.
|
||||
REMOTE = "REMOTE"
|
||||
|
||||
# Indicates a network router.
|
||||
ROUTER = "ROUTER"
|
||||
|
||||
@ -645,6 +650,24 @@ class FanCapabilities(AlexaEntity):
|
||||
yield Alexa(self.entity)
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(remote.DOMAIN)
|
||||
class RemoteCapabilities(AlexaEntity):
|
||||
"""Class to represent Remote capabilities."""
|
||||
|
||||
def default_display_categories(self) -> list[str]:
|
||||
"""Return the display categories for this entity."""
|
||||
return [DisplayCategory.REMOTE]
|
||||
|
||||
def interfaces(self) -> Generator[AlexaCapability]:
|
||||
"""Yield the supported interfaces."""
|
||||
yield AlexaPowerController(self.entity)
|
||||
yield AlexaModeController(
|
||||
self.entity, instance=f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}"
|
||||
)
|
||||
yield AlexaEndpointHealth(self.hass, self.entity)
|
||||
yield Alexa(self.entity)
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(humidifier.DOMAIN)
|
||||
class HumidifierCapabilities(AlexaEntity):
|
||||
"""Class to represent Humidifier capabilities."""
|
||||
|
@ -21,6 +21,7 @@ from homeassistant.components import (
|
||||
light,
|
||||
media_player,
|
||||
number,
|
||||
remote,
|
||||
timer,
|
||||
vacuum,
|
||||
valve,
|
||||
@ -185,6 +186,8 @@ async def async_api_turn_on(
|
||||
service = fan.SERVICE_TURN_ON
|
||||
elif domain == humidifier.DOMAIN:
|
||||
service = humidifier.SERVICE_TURN_ON
|
||||
elif domain == remote.DOMAIN:
|
||||
service = remote.SERVICE_TURN_ON
|
||||
elif domain == vacuum.DOMAIN:
|
||||
supported = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||
if (
|
||||
@ -234,6 +237,8 @@ async def async_api_turn_off(
|
||||
service = climate.SERVICE_TURN_OFF
|
||||
elif domain == fan.DOMAIN:
|
||||
service = fan.SERVICE_TURN_OFF
|
||||
elif domain == remote.DOMAIN:
|
||||
service = remote.SERVICE_TURN_OFF
|
||||
elif domain == humidifier.DOMAIN:
|
||||
service = humidifier.SERVICE_TURN_OFF
|
||||
elif domain == vacuum.DOMAIN:
|
||||
@ -1200,6 +1205,17 @@ async def async_api_set_mode(
|
||||
msg = f"Entity '{entity.entity_id}' does not support Mode '{mode}'"
|
||||
raise AlexaInvalidValueError(msg)
|
||||
|
||||
# Remote Activity
|
||||
if instance == f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}":
|
||||
activity = mode.split(".")[1]
|
||||
activities: list[str] | None = entity.attributes.get(remote.ATTR_ACTIVITY_LIST)
|
||||
if activity != PRESET_MODE_NA and activities and activity in activities:
|
||||
service = remote.SERVICE_TURN_ON
|
||||
data[remote.ATTR_ACTIVITY] = activity
|
||||
else:
|
||||
msg = f"Entity '{entity.entity_id}' does not support Mode '{mode}'"
|
||||
raise AlexaInvalidValueError(msg)
|
||||
|
||||
# Water heater operation mode
|
||||
elif instance == f"{water_heater.DOMAIN}.{water_heater.ATTR_OPERATION_MODE}":
|
||||
operation_mode = mode.split(".")[1]
|
||||
|
@ -48,6 +48,41 @@ from .test_common import (
|
||||
from tests.common import async_mock_service
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"curr_activity",
|
||||
"activity_list",
|
||||
),
|
||||
[
|
||||
("TV", ["TV", "MUSIC", "DVD"]),
|
||||
("TV", ["TV"]),
|
||||
],
|
||||
)
|
||||
async def test_discovery_remote(
|
||||
hass: HomeAssistant, curr_activity: str, activity_list: list[str]
|
||||
) -> None:
|
||||
"""Test discory for a remote entity."""
|
||||
request = get_new_request("Alexa.Discovery", "Discover")
|
||||
# setup test device
|
||||
hass.states.async_set(
|
||||
"remote.test",
|
||||
"off",
|
||||
{
|
||||
"current_activity": curr_activity,
|
||||
"activity_list": activity_list,
|
||||
},
|
||||
)
|
||||
msg = await smart_home.async_handle_message(hass, get_default_config(hass), request)
|
||||
assert "event" in msg
|
||||
msg = msg["event"]
|
||||
assert len(msg["payload"]["endpoints"]) == 1
|
||||
endpoint = msg["payload"]["endpoints"][0]
|
||||
assert endpoint["endpointId"] == "remote#test"
|
||||
interfaces = {capability["interface"] for capability in endpoint["capabilities"]}
|
||||
assert "Alexa.PowerController" in interfaces
|
||||
assert "Alexa.ModeController" in interfaces
|
||||
|
||||
|
||||
@pytest.mark.parametrize("adjust", ["-5", "5", "-80"])
|
||||
async def test_api_adjust_brightness(hass: HomeAssistant, adjust: str) -> None:
|
||||
"""Test api adjust brightness process."""
|
||||
@ -243,6 +278,102 @@ async def test_api_select_input(
|
||||
assert call.data["source"] == source_list[idx]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"target_activity",
|
||||
"activity_list",
|
||||
"current_activity_index",
|
||||
"target_activity_index",
|
||||
),
|
||||
[
|
||||
("TV", ["TV", "MUSIC", "DVD"], 1, 0),
|
||||
("MUSIC", ["TV", "MUSIC", "DVD", 1000], 0, 1),
|
||||
("DVD", ["TV", "MUSIC", "DVD", None], 0, 2),
|
||||
("BAD DEVICE", ["TV", "MUSIC", "DVD"], 0, None),
|
||||
("TV", ["TV"], 0, 0),
|
||||
("BAD DEVICE", [], None, None),
|
||||
],
|
||||
)
|
||||
async def test_api_select_activity(
|
||||
hass: HomeAssistant,
|
||||
target_activity: str,
|
||||
activity_list: list[str],
|
||||
current_activity_index: int | None,
|
||||
target_activity_index: int | None,
|
||||
) -> None:
|
||||
"""Test api set activity process."""
|
||||
curr_activty = (
|
||||
activity_list[current_activity_index] if current_activity_index else "None"
|
||||
)
|
||||
hass.states.async_set(
|
||||
"remote.test",
|
||||
"off",
|
||||
{
|
||||
"current_activity": curr_activty,
|
||||
"activity_list": activity_list,
|
||||
},
|
||||
)
|
||||
# test where no source matches
|
||||
if target_activity_index is None:
|
||||
await assert_request_fails(
|
||||
"Alexa.ModeController",
|
||||
"SetMode",
|
||||
"remote#test",
|
||||
"remote.turn_on",
|
||||
hass,
|
||||
payload={"mode": f"activity.{target_activity}"},
|
||||
instance="remote.activity",
|
||||
)
|
||||
return
|
||||
|
||||
call, _ = await assert_request_calls_service(
|
||||
"Alexa.ModeController",
|
||||
"SetMode",
|
||||
"remote#test",
|
||||
"remote.turn_on",
|
||||
hass,
|
||||
payload={"mode": f"activity.{target_activity}"},
|
||||
instance="remote.activity",
|
||||
)
|
||||
assert call.data["activity"] == activity_list[target_activity_index]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"curr_state",
|
||||
"target_name",
|
||||
"target_service",
|
||||
),
|
||||
[
|
||||
("on", "TurnOff", "turn_off"),
|
||||
("off", "TurnOn", "turn_on"),
|
||||
],
|
||||
)
|
||||
async def test_api_remote_set_power_state(
|
||||
hass: HomeAssistant,
|
||||
curr_state: str,
|
||||
target_name: str,
|
||||
target_service: str,
|
||||
) -> None:
|
||||
"""Test api remote set power state process."""
|
||||
hass.states.async_set(
|
||||
"remote.test",
|
||||
curr_state,
|
||||
{
|
||||
"current_activity": ["TV", "MUSIC", "DVD"],
|
||||
"activity_list": "TV",
|
||||
},
|
||||
)
|
||||
|
||||
_, msg = await assert_request_calls_service(
|
||||
"Alexa.PowerController",
|
||||
target_name,
|
||||
"remote#test",
|
||||
f"remote.{target_service}",
|
||||
hass,
|
||||
)
|
||||
|
||||
|
||||
async def test_report_lock_state(hass: HomeAssistant) -> None:
|
||||
"""Test LockController implements lockState property."""
|
||||
hass.states.async_set("lock.locked", STATE_LOCKED, {})
|
||||
@ -619,6 +750,62 @@ async def test_report_fan_direction(hass: HomeAssistant) -> None:
|
||||
properties.assert_equal("Alexa.ModeController", "mode", "direction.forward")
|
||||
|
||||
|
||||
async def test_report_remote_power(hass: HomeAssistant) -> None:
|
||||
"""Test ModeController reports remote power state correctly."""
|
||||
hass.states.async_set(
|
||||
"remote.off",
|
||||
"off",
|
||||
{"current_activity": "TV", "activity_list": ["TV", "MUSIC", "DVD"]},
|
||||
)
|
||||
hass.states.async_set(
|
||||
"remote.on",
|
||||
"on",
|
||||
{"current_activity": "TV", "activity_list": ["TV", "MUSIC", "DVD"]},
|
||||
)
|
||||
|
||||
properties = await reported_properties(hass, "remote#off")
|
||||
properties.assert_equal("Alexa.PowerController", "powerState", "OFF")
|
||||
|
||||
properties = await reported_properties(hass, "remote#on")
|
||||
properties.assert_equal("Alexa.PowerController", "powerState", "ON")
|
||||
|
||||
|
||||
async def test_report_remote_activity(hass: HomeAssistant) -> None:
|
||||
"""Test ModeController reports remote activity correctly."""
|
||||
hass.states.async_set(
|
||||
"remote.unknown",
|
||||
"on",
|
||||
{"current_activity": "UNKNOWN"},
|
||||
)
|
||||
hass.states.async_set(
|
||||
"remote.tv",
|
||||
"on",
|
||||
{"current_activity": "TV", "activity_list": ["TV", "MUSIC", "DVD"]},
|
||||
)
|
||||
hass.states.async_set(
|
||||
"remote.music",
|
||||
"on",
|
||||
{"current_activity": "MUSIC", "activity_list": ["TV", "MUSIC", "DVD"]},
|
||||
)
|
||||
hass.states.async_set(
|
||||
"remote.dvd",
|
||||
"on",
|
||||
{"current_activity": "DVD", "activity_list": ["TV", "MUSIC", "DVD"]},
|
||||
)
|
||||
|
||||
properties = await reported_properties(hass, "remote#unknown")
|
||||
properties.assert_not_has_property("Alexa.ModeController", "mode")
|
||||
|
||||
properties = await reported_properties(hass, "remote#tv")
|
||||
properties.assert_equal("Alexa.ModeController", "mode", "activity.TV")
|
||||
|
||||
properties = await reported_properties(hass, "remote#music")
|
||||
properties.assert_equal("Alexa.ModeController", "mode", "activity.MUSIC")
|
||||
|
||||
properties = await reported_properties(hass, "remote#dvd")
|
||||
properties.assert_equal("Alexa.ModeController", "mode", "activity.DVD")
|
||||
|
||||
|
||||
async def test_report_cover_range_value(hass: HomeAssistant) -> None:
|
||||
"""Test RangeController reports cover position correctly."""
|
||||
hass.states.async_set(
|
||||
|
Loading…
x
Reference in New Issue
Block a user