mirror of
https://github.com/home-assistant/core.git
synced 2025-07-24 21:57:51 +00:00
Make Alexa custom ID unique (#44839)
* Make Alexa custom ID unique * Lint * Lint
This commit is contained in:
parent
16e1046dbc
commit
69b5176730
@ -45,6 +45,11 @@ class AbstractConfig(ABC):
|
|||||||
"""Return if proactive mode is enabled."""
|
"""Return if proactive mode is enabled."""
|
||||||
return self._unsub_proactive_report is not None
|
return self._unsub_proactive_report is not None
|
||||||
|
|
||||||
|
@callback
|
||||||
|
@abstractmethod
|
||||||
|
def user_identifier(self):
|
||||||
|
"""Return an identifier for the user that represents this config."""
|
||||||
|
|
||||||
async def async_enable_proactive_mode(self):
|
async def async_enable_proactive_mode(self):
|
||||||
"""Enable proactive mode."""
|
"""Enable proactive mode."""
|
||||||
if self._unsub_proactive_report is None:
|
if self._unsub_proactive_report is None:
|
||||||
|
@ -329,7 +329,7 @@ class AlexaEntity:
|
|||||||
"manufacturer": "Home Assistant",
|
"manufacturer": "Home Assistant",
|
||||||
"model": self.entity.domain,
|
"model": self.entity.domain,
|
||||||
"softwareVersion": __version__,
|
"softwareVersion": __version__,
|
||||||
"customIdentifier": self.entity_id,
|
"customIdentifier": f"{self.config.user_identifier()}-{self.entity_id}",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,6 +53,11 @@ class AlexaConfig(AbstractConfig):
|
|||||||
"""Return config locale."""
|
"""Return config locale."""
|
||||||
return self._config.get(CONF_LOCALE)
|
return self._config.get(CONF_LOCALE)
|
||||||
|
|
||||||
|
@core.callback
|
||||||
|
def user_identifier(self):
|
||||||
|
"""Return an identifier for the user that represents this config."""
|
||||||
|
return ""
|
||||||
|
|
||||||
def should_expose(self, entity_id):
|
def should_expose(self, entity_id):
|
||||||
"""If an entity should be exposed."""
|
"""If an entity should be exposed."""
|
||||||
return self._config[CONF_FILTER](entity_id)
|
return self._config[CONF_FILTER](entity_id)
|
||||||
|
@ -5,7 +5,7 @@ import logging
|
|||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import async_timeout
|
import async_timeout
|
||||||
from hass_nabucasa import cloud_api
|
from hass_nabucasa import Cloud, cloud_api
|
||||||
|
|
||||||
from homeassistant.components.alexa import (
|
from homeassistant.components.alexa import (
|
||||||
config as alexa_config,
|
config as alexa_config,
|
||||||
@ -14,7 +14,7 @@ from homeassistant.components.alexa import (
|
|||||||
state_report as alexa_state_report,
|
state_report as alexa_state_report,
|
||||||
)
|
)
|
||||||
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES, HTTP_BAD_REQUEST
|
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES, HTTP_BAD_REQUEST
|
||||||
from homeassistant.core import callback, split_entity_id
|
from homeassistant.core import HomeAssistant, callback, split_entity_id
|
||||||
from homeassistant.helpers import entity_registry
|
from homeassistant.helpers import entity_registry
|
||||||
from homeassistant.helpers.event import async_call_later
|
from homeassistant.helpers.event import async_call_later
|
||||||
from homeassistant.util.dt import utcnow
|
from homeassistant.util.dt import utcnow
|
||||||
@ -32,10 +32,18 @@ SYNC_DELAY = 1
|
|||||||
class AlexaConfig(alexa_config.AbstractConfig):
|
class AlexaConfig(alexa_config.AbstractConfig):
|
||||||
"""Alexa Configuration."""
|
"""Alexa Configuration."""
|
||||||
|
|
||||||
def __init__(self, hass, config, prefs: CloudPreferences, cloud):
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config: dict,
|
||||||
|
cloud_user: str,
|
||||||
|
prefs: CloudPreferences,
|
||||||
|
cloud: Cloud,
|
||||||
|
):
|
||||||
"""Initialize the Alexa config."""
|
"""Initialize the Alexa config."""
|
||||||
super().__init__(hass)
|
super().__init__(hass)
|
||||||
self._config = config
|
self._config = config
|
||||||
|
self._cloud_user = cloud_user
|
||||||
self._prefs = prefs
|
self._prefs = prefs
|
||||||
self._cloud = cloud
|
self._cloud = cloud
|
||||||
self._token = None
|
self._token = None
|
||||||
@ -85,6 +93,11 @@ class AlexaConfig(alexa_config.AbstractConfig):
|
|||||||
"""Return entity config."""
|
"""Return entity config."""
|
||||||
return self._config.get(CONF_ENTITY_CONFIG) or {}
|
return self._config.get(CONF_ENTITY_CONFIG) or {}
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def user_identifier(self):
|
||||||
|
"""Return an identifier for the user that represents this config."""
|
||||||
|
return self._cloud_user
|
||||||
|
|
||||||
def should_expose(self, entity_id):
|
def should_expose(self, entity_id):
|
||||||
"""If an entity should be exposed."""
|
"""If an entity should be exposed."""
|
||||||
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
|
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
|
||||||
|
@ -79,13 +79,15 @@ class CloudClient(Interface):
|
|||||||
"""Return true if we want start a remote connection."""
|
"""Return true if we want start a remote connection."""
|
||||||
return self._prefs.remote_enabled
|
return self._prefs.remote_enabled
|
||||||
|
|
||||||
@property
|
async def get_alexa_config(self) -> alexa_config.AlexaConfig:
|
||||||
def alexa_config(self) -> alexa_config.AlexaConfig:
|
|
||||||
"""Return Alexa config."""
|
"""Return Alexa config."""
|
||||||
if self._alexa_config is None:
|
if self._alexa_config is None:
|
||||||
assert self.cloud is not None
|
assert self.cloud is not None
|
||||||
|
|
||||||
|
cloud_user = await self._prefs.get_cloud_user()
|
||||||
|
|
||||||
self._alexa_config = alexa_config.AlexaConfig(
|
self._alexa_config = alexa_config.AlexaConfig(
|
||||||
self._hass, self.alexa_user_config, self._prefs, self.cloud
|
self._hass, self.alexa_user_config, cloud_user, self._prefs, self.cloud
|
||||||
)
|
)
|
||||||
|
|
||||||
return self._alexa_config
|
return self._alexa_config
|
||||||
@ -110,8 +112,9 @@ class CloudClient(Interface):
|
|||||||
|
|
||||||
async def enable_alexa(_):
|
async def enable_alexa(_):
|
||||||
"""Enable Alexa."""
|
"""Enable Alexa."""
|
||||||
|
aconf = await self.get_alexa_config()
|
||||||
try:
|
try:
|
||||||
await self.alexa_config.async_enable_proactive_mode()
|
await aconf.async_enable_proactive_mode()
|
||||||
except aiohttp.ClientError as err: # If no internet available yet
|
except aiohttp.ClientError as err: # If no internet available yet
|
||||||
if self._hass.is_running:
|
if self._hass.is_running:
|
||||||
logging.getLogger(__package__).warning(
|
logging.getLogger(__package__).warning(
|
||||||
@ -133,7 +136,7 @@ class CloudClient(Interface):
|
|||||||
|
|
||||||
tasks = []
|
tasks = []
|
||||||
|
|
||||||
if self.alexa_config.enabled and self.alexa_config.should_report_state:
|
if self._prefs.alexa_enabled and self._prefs.alexa_report_state:
|
||||||
tasks.append(enable_alexa)
|
tasks.append(enable_alexa)
|
||||||
|
|
||||||
if self._prefs.google_enabled:
|
if self._prefs.google_enabled:
|
||||||
@ -164,9 +167,10 @@ class CloudClient(Interface):
|
|||||||
async def async_alexa_message(self, payload: Dict[Any, Any]) -> Dict[Any, Any]:
|
async def async_alexa_message(self, payload: Dict[Any, Any]) -> Dict[Any, Any]:
|
||||||
"""Process cloud alexa message to client."""
|
"""Process cloud alexa message to client."""
|
||||||
cloud_user = await self._prefs.get_cloud_user()
|
cloud_user = await self._prefs.get_cloud_user()
|
||||||
|
aconfig = await self.get_alexa_config()
|
||||||
return await alexa_sh.async_handle_message(
|
return await alexa_sh.async_handle_message(
|
||||||
self._hass,
|
self._hass,
|
||||||
self.alexa_config,
|
aconfig,
|
||||||
payload,
|
payload,
|
||||||
context=Context(user_id=cloud_user),
|
context=Context(user_id=cloud_user),
|
||||||
enabled=self._prefs.alexa_enabled,
|
enabled=self._prefs.alexa_enabled,
|
||||||
|
@ -28,7 +28,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
class CloudGoogleConfig(AbstractConfig):
|
class CloudGoogleConfig(AbstractConfig):
|
||||||
"""HA Cloud Configuration for Google Assistant."""
|
"""HA Cloud Configuration for Google Assistant."""
|
||||||
|
|
||||||
def __init__(self, hass, config, cloud_user, prefs: CloudPreferences, cloud):
|
def __init__(self, hass, config, cloud_user: str, prefs: CloudPreferences, cloud):
|
||||||
"""Initialize the Google config."""
|
"""Initialize the Google config."""
|
||||||
super().__init__(hass)
|
super().__init__(hass)
|
||||||
self._config = config
|
self._config = config
|
||||||
|
@ -397,9 +397,10 @@ async def websocket_update_prefs(hass, connection, msg):
|
|||||||
|
|
||||||
# If we turn alexa linking on, validate that we can fetch access token
|
# If we turn alexa linking on, validate that we can fetch access token
|
||||||
if changes.get(PREF_ALEXA_REPORT_STATE):
|
if changes.get(PREF_ALEXA_REPORT_STATE):
|
||||||
|
alexa_config = await cloud.client.get_alexa_config()
|
||||||
try:
|
try:
|
||||||
with async_timeout.timeout(10):
|
with async_timeout.timeout(10):
|
||||||
await cloud.client.alexa_config.async_get_access_token()
|
await alexa_config.async_get_access_token()
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
connection.send_error(
|
connection.send_error(
|
||||||
msg["id"], "alexa_timeout", "Timeout validating Alexa access token."
|
msg["id"], "alexa_timeout", "Timeout validating Alexa access token."
|
||||||
@ -555,7 +556,8 @@ async def google_assistant_update(hass, connection, msg):
|
|||||||
async def alexa_list(hass, connection, msg):
|
async def alexa_list(hass, connection, msg):
|
||||||
"""List all alexa entities."""
|
"""List all alexa entities."""
|
||||||
cloud = hass.data[DOMAIN]
|
cloud = hass.data[DOMAIN]
|
||||||
entities = alexa_entities.async_get_entities(hass, cloud.client.alexa_config)
|
alexa_config = await cloud.client.get_alexa_config()
|
||||||
|
entities = alexa_entities.async_get_entities(hass, alexa_config)
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
|
|
||||||
@ -603,10 +605,11 @@ async def alexa_update(hass, connection, msg):
|
|||||||
async def alexa_sync(hass, connection, msg):
|
async def alexa_sync(hass, connection, msg):
|
||||||
"""Sync with Alexa."""
|
"""Sync with Alexa."""
|
||||||
cloud = hass.data[DOMAIN]
|
cloud = hass.data[DOMAIN]
|
||||||
|
alexa_config = await cloud.client.get_alexa_config()
|
||||||
|
|
||||||
with async_timeout.timeout(10):
|
with async_timeout.timeout(10):
|
||||||
try:
|
try:
|
||||||
success = await cloud.client.alexa_config.async_sync_entities()
|
success = await alexa_config.async_sync_entities()
|
||||||
except alexa_errors.NoTokenAvailable:
|
except alexa_errors.NoTokenAvailable:
|
||||||
connection.send_error(
|
connection.send_error(
|
||||||
msg["id"],
|
msg["id"],
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from homeassistant.components.alexa import config, smart_home
|
from homeassistant.components.alexa import config, smart_home
|
||||||
from homeassistant.core import Context
|
from homeassistant.core import Context, callback
|
||||||
|
|
||||||
from tests.common import async_mock_service
|
from tests.common import async_mock_service
|
||||||
|
|
||||||
@ -37,6 +37,11 @@ class MockConfig(config.AbstractConfig):
|
|||||||
"""Return config locale."""
|
"""Return config locale."""
|
||||||
return TEST_LOCALE
|
return TEST_LOCALE
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def user_identifier(self):
|
||||||
|
"""Return an identifier for the user that represents this config."""
|
||||||
|
return "mock-user-id"
|
||||||
|
|
||||||
def should_expose(self, entity_id):
|
def should_expose(self, entity_id):
|
||||||
"""If an entity should be exposed."""
|
"""If an entity should be exposed."""
|
||||||
return True
|
return True
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
from homeassistant.components.alexa import smart_home
|
from homeassistant.components.alexa import smart_home
|
||||||
|
from homeassistant.const import __version__
|
||||||
|
|
||||||
from . import DEFAULT_CONFIG, get_new_request
|
from . import DEFAULT_CONFIG, get_new_request
|
||||||
|
|
||||||
@ -20,6 +21,26 @@ async def test_unsupported_domain(hass):
|
|||||||
assert not msg["payload"]["endpoints"]
|
assert not msg["payload"]["endpoints"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_serialize_discovery(hass):
|
||||||
|
"""Test we handle an interface raising unexpectedly during serialize discovery."""
|
||||||
|
request = get_new_request("Alexa.Discovery", "Discover")
|
||||||
|
|
||||||
|
hass.states.async_set("switch.bla", "on", {"friendly_name": "Boop Woz"})
|
||||||
|
|
||||||
|
msg = await smart_home.async_handle_message(hass, DEFAULT_CONFIG, request)
|
||||||
|
|
||||||
|
assert "event" in msg
|
||||||
|
msg = msg["event"]
|
||||||
|
endpoint = msg["payload"]["endpoints"][0]
|
||||||
|
|
||||||
|
assert endpoint["additionalAttributes"] == {
|
||||||
|
"manufacturer": "Home Assistant",
|
||||||
|
"model": "switch",
|
||||||
|
"softwareVersion": __version__,
|
||||||
|
"customIdentifier": "mock-user-id-switch.bla",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def test_serialize_discovery_recovers(hass, caplog):
|
async def test_serialize_discovery_recovers(hass, caplog):
|
||||||
"""Test we handle an interface raising unexpectedly during serialize discovery."""
|
"""Test we handle an interface raising unexpectedly during serialize discovery."""
|
||||||
request = get_new_request("Alexa.Discovery", "Discover")
|
request = get_new_request("Alexa.Discovery", "Discover")
|
||||||
|
@ -16,7 +16,9 @@ async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs):
|
|||||||
alexa_entity_configs={"light.kitchen": entity_conf},
|
alexa_entity_configs={"light.kitchen": entity_conf},
|
||||||
alexa_default_expose=["light"],
|
alexa_default_expose=["light"],
|
||||||
)
|
)
|
||||||
conf = alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None)
|
conf = alexa_config.AlexaConfig(
|
||||||
|
hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, None
|
||||||
|
)
|
||||||
|
|
||||||
assert not conf.should_expose("light.kitchen")
|
assert not conf.should_expose("light.kitchen")
|
||||||
entity_conf["should_expose"] = True
|
entity_conf["should_expose"] = True
|
||||||
@ -33,7 +35,9 @@ async def test_alexa_config_expose_entity_prefs(hass, cloud_prefs):
|
|||||||
|
|
||||||
async def test_alexa_config_report_state(hass, cloud_prefs):
|
async def test_alexa_config_report_state(hass, cloud_prefs):
|
||||||
"""Test Alexa config should expose using prefs."""
|
"""Test Alexa config should expose using prefs."""
|
||||||
conf = alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None)
|
conf = alexa_config.AlexaConfig(
|
||||||
|
hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, None
|
||||||
|
)
|
||||||
|
|
||||||
assert cloud_prefs.alexa_report_state is False
|
assert cloud_prefs.alexa_report_state is False
|
||||||
assert conf.should_report_state is False
|
assert conf.should_report_state is False
|
||||||
@ -68,6 +72,7 @@ async def test_alexa_config_invalidate_token(hass, cloud_prefs, aioclient_mock):
|
|||||||
conf = alexa_config.AlexaConfig(
|
conf = alexa_config.AlexaConfig(
|
||||||
hass,
|
hass,
|
||||||
ALEXA_SCHEMA({}),
|
ALEXA_SCHEMA({}),
|
||||||
|
"mock-user-id",
|
||||||
cloud_prefs,
|
cloud_prefs,
|
||||||
Mock(
|
Mock(
|
||||||
alexa_access_token_url="http://example/alexa_token",
|
alexa_access_token_url="http://example/alexa_token",
|
||||||
@ -114,7 +119,7 @@ def patch_sync_helper():
|
|||||||
|
|
||||||
async def test_alexa_update_expose_trigger_sync(hass, cloud_prefs):
|
async def test_alexa_update_expose_trigger_sync(hass, cloud_prefs):
|
||||||
"""Test Alexa config responds to updating exposed entities."""
|
"""Test Alexa config responds to updating exposed entities."""
|
||||||
alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None)
|
alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, None)
|
||||||
|
|
||||||
with patch_sync_helper() as (to_update, to_remove):
|
with patch_sync_helper() as (to_update, to_remove):
|
||||||
await cloud_prefs.async_update_alexa_entity_config(
|
await cloud_prefs.async_update_alexa_entity_config(
|
||||||
@ -147,7 +152,9 @@ async def test_alexa_update_expose_trigger_sync(hass, cloud_prefs):
|
|||||||
|
|
||||||
async def test_alexa_entity_registry_sync(hass, mock_cloud_login, cloud_prefs):
|
async def test_alexa_entity_registry_sync(hass, mock_cloud_login, cloud_prefs):
|
||||||
"""Test Alexa config responds to entity registry."""
|
"""Test Alexa config responds to entity registry."""
|
||||||
alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, hass.data["cloud"])
|
alexa_config.AlexaConfig(
|
||||||
|
hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, hass.data["cloud"]
|
||||||
|
)
|
||||||
|
|
||||||
with patch_sync_helper() as (to_update, to_remove):
|
with patch_sync_helper() as (to_update, to_remove):
|
||||||
hass.bus.async_fire(
|
hass.bus.async_fire(
|
||||||
@ -197,7 +204,7 @@ async def test_alexa_entity_registry_sync(hass, mock_cloud_login, cloud_prefs):
|
|||||||
|
|
||||||
async def test_alexa_update_report_state(hass, cloud_prefs):
|
async def test_alexa_update_report_state(hass, cloud_prefs):
|
||||||
"""Test Alexa config responds to reporting state."""
|
"""Test Alexa config responds to reporting state."""
|
||||||
alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), cloud_prefs, None)
|
alexa_config.AlexaConfig(hass, ALEXA_SCHEMA({}), "mock-user-id", cloud_prefs, None)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.cloud.alexa_config.AlexaConfig.async_sync_entities",
|
"homeassistant.components.cloud.alexa_config.AlexaConfig.async_sync_entities",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user