mirror of
https://github.com/home-assistant/core.git
synced 2025-07-26 06:37:52 +00:00
Enable strict type checks of cloud (#92576)
* Enable strict type checks of cloud * Fix circular import * Address review comments * Apply suggestions from code review
This commit is contained in:
parent
a8ce4d3c66
commit
937ebb374c
@ -87,6 +87,7 @@ homeassistant.components.camera.*
|
|||||||
homeassistant.components.canary.*
|
homeassistant.components.canary.*
|
||||||
homeassistant.components.clickatell.*
|
homeassistant.components.clickatell.*
|
||||||
homeassistant.components.clicksend.*
|
homeassistant.components.clicksend.*
|
||||||
|
homeassistant.components.cloud.*
|
||||||
homeassistant.components.configurator.*
|
homeassistant.components.configurator.*
|
||||||
homeassistant.components.cover.*
|
homeassistant.components.cover.*
|
||||||
homeassistant.components.cpuspeed.*
|
homeassistant.components.cpuspeed.*
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
"""Config helpers for Alexa."""
|
"""Config helpers for Alexa."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
@ -17,15 +19,15 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
class AbstractConfig(ABC):
|
class AbstractConfig(ABC):
|
||||||
"""Hold the configuration for Alexa."""
|
"""Hold the configuration for Alexa."""
|
||||||
|
|
||||||
|
_store: AlexaConfigStore
|
||||||
_unsub_proactive_report: CALLBACK_TYPE | None = None
|
_unsub_proactive_report: CALLBACK_TYPE | None = None
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant) -> None:
|
def __init__(self, hass: HomeAssistant) -> None:
|
||||||
"""Initialize abstract config."""
|
"""Initialize abstract config."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self._enable_proactive_mode_lock = asyncio.Lock()
|
self._enable_proactive_mode_lock = asyncio.Lock()
|
||||||
self._store = None
|
|
||||||
|
|
||||||
async def async_initialize(self):
|
async def async_initialize(self) -> None:
|
||||||
"""Perform async initialization of config."""
|
"""Perform async initialization of config."""
|
||||||
self._store = AlexaConfigStore(self.hass)
|
self._store = AlexaConfigStore(self.hass)
|
||||||
await self._store.async_load()
|
await self._store.async_load()
|
||||||
@ -65,7 +67,7 @@ class AbstractConfig(ABC):
|
|||||||
def user_identifier(self):
|
def user_identifier(self):
|
||||||
"""Return an identifier for the user that represents this config."""
|
"""Return an identifier for the user that represents this config."""
|
||||||
|
|
||||||
async def async_enable_proactive_mode(self):
|
async def async_enable_proactive_mode(self) -> None:
|
||||||
"""Enable proactive mode."""
|
"""Enable proactive mode."""
|
||||||
_LOGGER.debug("Enable proactive mode")
|
_LOGGER.debug("Enable proactive mode")
|
||||||
async with self._enable_proactive_mode_lock:
|
async with self._enable_proactive_mode_lock:
|
||||||
@ -75,7 +77,7 @@ class AbstractConfig(ABC):
|
|||||||
self.hass, self
|
self.hass, self
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_disable_proactive_mode(self):
|
async def async_disable_proactive_mode(self) -> None:
|
||||||
"""Disable proactive mode."""
|
"""Disable proactive mode."""
|
||||||
_LOGGER.debug("Disable proactive mode")
|
_LOGGER.debug("Disable proactive mode")
|
||||||
if unsub_func := self._unsub_proactive_report:
|
if unsub_func := self._unsub_proactive_report:
|
||||||
@ -105,7 +107,7 @@ class AbstractConfig(ABC):
|
|||||||
"""Return authorization status."""
|
"""Return authorization status."""
|
||||||
return self._store.authorized
|
return self._store.authorized
|
||||||
|
|
||||||
async def set_authorized(self, authorized):
|
async def set_authorized(self, authorized) -> None:
|
||||||
"""Set authorization status.
|
"""Set authorization status.
|
||||||
|
|
||||||
- Set when an incoming message is received from Alexa.
|
- Set when an incoming message is received from Alexa.
|
||||||
|
@ -5,7 +5,7 @@ import asyncio
|
|||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import cast
|
from typing import TYPE_CHECKING, cast
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import async_timeout
|
import async_timeout
|
||||||
@ -23,6 +23,9 @@ from .entities import ENTITY_ADAPTERS, AlexaEntity, generate_alexa_id
|
|||||||
from .errors import NoTokenAvailable, RequireRelink
|
from .errors import NoTokenAvailable, RequireRelink
|
||||||
from .messages import AlexaResponse
|
from .messages import AlexaResponse
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .config import AbstractConfig
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
DEFAULT_TIMEOUT = 10
|
DEFAULT_TIMEOUT = 10
|
||||||
|
|
||||||
@ -188,7 +191,9 @@ async def async_send_changereport_message(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_send_add_or_update_message(hass, config, entity_ids):
|
async def async_send_add_or_update_message(
|
||||||
|
hass: HomeAssistant, config: AbstractConfig, entity_ids: list[str]
|
||||||
|
) -> aiohttp.ClientResponse:
|
||||||
"""Send an AddOrUpdateReport message for entities.
|
"""Send an AddOrUpdateReport message for entities.
|
||||||
|
|
||||||
https://developer.amazon.com/docs/device-apis/alexa-discovery.html#add-or-update-report
|
https://developer.amazon.com/docs/device-apis/alexa-discovery.html#add-or-update-report
|
||||||
@ -223,7 +228,9 @@ async def async_send_add_or_update_message(hass, config, entity_ids):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_send_delete_message(hass, config, entity_ids):
|
async def async_send_delete_message(
|
||||||
|
hass: HomeAssistant, config: AbstractConfig, entity_ids: list[str]
|
||||||
|
) -> aiohttp.ClientResponse:
|
||||||
"""Send an DeleteReport message for entities.
|
"""Send an DeleteReport message for entities.
|
||||||
|
|
||||||
https://developer.amazon.com/docs/device-apis/alexa-discovery.html#deletereport-event
|
https://developer.amazon.com/docs/device-apis/alexa-discovery.html#deletereport-event
|
||||||
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
from datetime import timedelta
|
from datetime import datetime, timedelta
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from hass_nabucasa import Cloud
|
from hass_nabucasa import Cloud
|
||||||
@ -18,7 +18,7 @@ from homeassistant.const import (
|
|||||||
EVENT_HOMEASSISTANT_STOP,
|
EVENT_HOMEASSISTANT_STOP,
|
||||||
Platform,
|
Platform,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback
|
from homeassistant.core import Event, HassJob, HomeAssistant, ServiceCall, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import config_validation as cv, entityfilter
|
from homeassistant.helpers import config_validation as cv, entityfilter
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
@ -31,7 +31,6 @@ from homeassistant.helpers.event import async_call_later
|
|||||||
from homeassistant.helpers.service import async_register_admin_service
|
from homeassistant.helpers.service import async_register_admin_service
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
from homeassistant.loader import bind_hass
|
from homeassistant.loader import bind_hass
|
||||||
from homeassistant.util.aiohttp import MockRequest
|
|
||||||
|
|
||||||
from . import account_link, http_api
|
from . import account_link, http_api
|
||||||
from .client import CloudClient
|
from .client import CloudClient
|
||||||
@ -184,8 +183,10 @@ async def async_create_cloudhook(hass: HomeAssistant, webhook_id: str) -> str:
|
|||||||
if not async_is_logged_in(hass):
|
if not async_is_logged_in(hass):
|
||||||
raise CloudNotAvailable
|
raise CloudNotAvailable
|
||||||
|
|
||||||
hook = await hass.data[DOMAIN].cloudhooks.async_create(webhook_id, True)
|
cloud: Cloud[CloudClient] = hass.data[DOMAIN]
|
||||||
return hook["cloudhook_url"]
|
hook = await cloud.cloudhooks.async_create(webhook_id, True)
|
||||||
|
cloudhook_url: str = hook["cloudhook_url"]
|
||||||
|
return cloudhook_url
|
||||||
|
|
||||||
|
|
||||||
@bind_hass
|
@bind_hass
|
||||||
@ -213,14 +214,6 @@ def async_remote_ui_url(hass: HomeAssistant) -> str:
|
|||||||
return f"https://{remote_domain}"
|
return f"https://{remote_domain}"
|
||||||
|
|
||||||
|
|
||||||
def is_cloudhook_request(request):
|
|
||||||
"""Test if a request came from a cloudhook.
|
|
||||||
|
|
||||||
Async friendly.
|
|
||||||
"""
|
|
||||||
return isinstance(request, MockRequest)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
"""Initialize the Home Assistant cloud."""
|
"""Initialize the Home Assistant cloud."""
|
||||||
# Process configs
|
# Process configs
|
||||||
@ -243,7 +236,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
cloud = hass.data[DOMAIN] = Cloud(client, **kwargs)
|
cloud = hass.data[DOMAIN] = Cloud(client, **kwargs)
|
||||||
cloud.iot.register_on_connect(client.on_cloud_connected)
|
cloud.iot.register_on_connect(client.on_cloud_connected)
|
||||||
|
|
||||||
async def _shutdown(event):
|
async def _shutdown(event: Event) -> None:
|
||||||
"""Shutdown event."""
|
"""Shutdown event."""
|
||||||
await cloud.stop()
|
await cloud.stop()
|
||||||
|
|
||||||
@ -263,7 +256,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
hass, DOMAIN, SERVICE_REMOTE_DISCONNECT, _service_handler
|
hass, DOMAIN, SERVICE_REMOTE_DISCONNECT, _service_handler
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_startup_repairs(_=None) -> None:
|
async def async_startup_repairs(_: datetime) -> None:
|
||||||
"""Create repair issues after startup."""
|
"""Create repair issues after startup."""
|
||||||
if not cloud.is_logged_in:
|
if not cloud.is_logged_in:
|
||||||
return
|
return
|
||||||
@ -273,7 +266,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
|
|
||||||
loaded = False
|
loaded = False
|
||||||
|
|
||||||
async def _on_start():
|
async def _on_start() -> None:
|
||||||
"""Discover platforms."""
|
"""Discover platforms."""
|
||||||
nonlocal loaded
|
nonlocal loaded
|
||||||
|
|
||||||
@ -292,19 +285,19 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
await async_load_platform(hass, Platform.TTS, DOMAIN, tts_info, config)
|
await async_load_platform(hass, Platform.TTS, DOMAIN, tts_info, config)
|
||||||
await asyncio.gather(stt_platform_loaded.wait(), tts_platform_loaded.wait())
|
await asyncio.gather(stt_platform_loaded.wait(), tts_platform_loaded.wait())
|
||||||
|
|
||||||
async def _on_connect():
|
async def _on_connect() -> None:
|
||||||
"""Handle cloud connect."""
|
"""Handle cloud connect."""
|
||||||
async_dispatcher_send(
|
async_dispatcher_send(
|
||||||
hass, SIGNAL_CLOUD_CONNECTION_STATE, CloudConnectionState.CLOUD_CONNECTED
|
hass, SIGNAL_CLOUD_CONNECTION_STATE, CloudConnectionState.CLOUD_CONNECTED
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _on_disconnect():
|
async def _on_disconnect() -> None:
|
||||||
"""Handle cloud disconnect."""
|
"""Handle cloud disconnect."""
|
||||||
async_dispatcher_send(
|
async_dispatcher_send(
|
||||||
hass, SIGNAL_CLOUD_CONNECTION_STATE, CloudConnectionState.CLOUD_DISCONNECTED
|
hass, SIGNAL_CLOUD_CONNECTION_STATE, CloudConnectionState.CLOUD_DISCONNECTED
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _on_initialized():
|
async def _on_initialized() -> None:
|
||||||
"""Update preferences."""
|
"""Update preferences."""
|
||||||
await prefs.async_update(remote_domain=cloud.remote.instance_domain)
|
await prefs.async_update(remote_domain=cloud.remote.instance_domain)
|
||||||
|
|
||||||
@ -330,7 +323,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _remote_handle_prefs_updated(cloud: Cloud) -> None:
|
def _remote_handle_prefs_updated(cloud: Cloud[CloudClient]) -> None:
|
||||||
"""Handle remote preferences updated."""
|
"""Handle remote preferences updated."""
|
||||||
cur_pref = cloud.client.prefs.remote_enabled
|
cur_pref = cloud.client.prefs.remote_enabled
|
||||||
lock = asyncio.Lock()
|
lock = asyncio.Lock()
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
"""Account linking via the cloud."""
|
"""Account linking via the cloud."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from datetime import datetime
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@ -24,14 +27,16 @@ CURRENT_PLAIN_VERSION = AwesomeVersion(
|
|||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_setup(hass: HomeAssistant):
|
def async_setup(hass: HomeAssistant) -> None:
|
||||||
"""Set up cloud account link."""
|
"""Set up cloud account link."""
|
||||||
config_entry_oauth2_flow.async_add_implementation_provider(
|
config_entry_oauth2_flow.async_add_implementation_provider(
|
||||||
hass, DOMAIN, async_provide_implementation
|
hass, DOMAIN, async_provide_implementation
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_provide_implementation(hass: HomeAssistant, domain: str):
|
async def async_provide_implementation(
|
||||||
|
hass: HomeAssistant, domain: str
|
||||||
|
) -> list[config_entry_oauth2_flow.AbstractOAuth2Implementation]:
|
||||||
"""Provide an implementation for a domain."""
|
"""Provide an implementation for a domain."""
|
||||||
services = await _get_services(hass)
|
services = await _get_services(hass)
|
||||||
|
|
||||||
@ -55,9 +60,11 @@ async def async_provide_implementation(hass: HomeAssistant, domain: str):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
async def _get_services(hass):
|
async def _get_services(hass: HomeAssistant) -> list[dict[str, Any]]:
|
||||||
"""Get the available services."""
|
"""Get the available services."""
|
||||||
if (services := hass.data.get(DATA_SERVICES)) is not None:
|
services: list[dict[str, Any]]
|
||||||
|
if DATA_SERVICES in hass.data:
|
||||||
|
services = hass.data[DATA_SERVICES]
|
||||||
return services
|
return services
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -68,7 +75,7 @@ async def _get_services(hass):
|
|||||||
hass.data[DATA_SERVICES] = services
|
hass.data[DATA_SERVICES] = services
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def clear_services(_now):
|
def clear_services(_now: datetime) -> None:
|
||||||
"""Clear services cache."""
|
"""Clear services cache."""
|
||||||
hass.data.pop(DATA_SERVICES, None)
|
hass.data.pop(DATA_SERVICES, None)
|
||||||
|
|
||||||
@ -102,7 +109,7 @@ class CloudOAuth2Implementation(config_entry_oauth2_flow.AbstractOAuth2Implement
|
|||||||
)
|
)
|
||||||
authorize_url = await helper.async_get_authorize_url()
|
authorize_url = await helper.async_get_authorize_url()
|
||||||
|
|
||||||
async def await_tokens():
|
async def await_tokens() -> None:
|
||||||
"""Wait for tokens and pass them on when received."""
|
"""Wait for tokens and pass them on when received."""
|
||||||
try:
|
try:
|
||||||
tokens = await helper.async_get_tokens()
|
tokens = await helper.async_get_tokens()
|
||||||
@ -125,7 +132,8 @@ class CloudOAuth2Implementation(config_entry_oauth2_flow.AbstractOAuth2Implement
|
|||||||
async def async_resolve_external_data(self, external_data: Any) -> dict:
|
async def async_resolve_external_data(self, external_data: Any) -> dict:
|
||||||
"""Resolve external data to tokens."""
|
"""Resolve external data to tokens."""
|
||||||
# We already passed in tokens
|
# We already passed in tokens
|
||||||
return external_data
|
dict_data: dict = external_data
|
||||||
|
return dict_data
|
||||||
|
|
||||||
async def _async_refresh_token(self, token: dict) -> dict:
|
async def _async_refresh_token(self, token: dict) -> dict:
|
||||||
"""Refresh a token."""
|
"""Refresh a token."""
|
||||||
|
@ -4,9 +4,10 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from datetime import timedelta
|
from datetime import datetime, timedelta
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
import logging
|
import logging
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import async_timeout
|
import async_timeout
|
||||||
@ -29,10 +30,11 @@ from homeassistant.components.homeassistant.exposed_entities import (
|
|||||||
)
|
)
|
||||||
from homeassistant.components.sensor import SensorDeviceClass
|
from homeassistant.components.sensor import SensorDeviceClass
|
||||||
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
|
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
|
||||||
from homeassistant.core import HomeAssistant, callback, split_entity_id
|
from homeassistant.core import Event, HomeAssistant, callback, split_entity_id
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import entity_registry as er, start
|
from homeassistant.helpers import entity_registry as er, start
|
||||||
from homeassistant.helpers.entity import get_device_class
|
from homeassistant.helpers.entity import get_device_class
|
||||||
|
from homeassistant.helpers.entityfilter import EntityFilter
|
||||||
from homeassistant.helpers.event import async_call_later
|
from homeassistant.helpers.event import async_call_later
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
from homeassistant.util.dt import utcnow
|
from homeassistant.util.dt import utcnow
|
||||||
@ -47,6 +49,9 @@ from .const import (
|
|||||||
)
|
)
|
||||||
from .prefs import ALEXA_SETTINGS_VERSION, CloudPreferences
|
from .prefs import ALEXA_SETTINGS_VERSION, CloudPreferences
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .client import CloudClient
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CLOUD_ALEXA = f"{CLOUD_DOMAIN}.{ALEXA_DOMAIN}"
|
CLOUD_ALEXA = f"{CLOUD_DOMAIN}.{ALEXA_DOMAIN}"
|
||||||
@ -132,7 +137,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
|||||||
config: dict,
|
config: dict,
|
||||||
cloud_user: str,
|
cloud_user: str,
|
||||||
prefs: CloudPreferences,
|
prefs: CloudPreferences,
|
||||||
cloud: Cloud,
|
cloud: Cloud[CloudClient],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the Alexa config."""
|
"""Initialize the Alexa config."""
|
||||||
super().__init__(hass)
|
super().__init__(hass)
|
||||||
@ -141,13 +146,13 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
|||||||
self._prefs = prefs
|
self._prefs = prefs
|
||||||
self._cloud = cloud
|
self._cloud = cloud
|
||||||
self._token = None
|
self._token = None
|
||||||
self._token_valid = None
|
self._token_valid: datetime | None = None
|
||||||
self._cur_entity_prefs = async_get_assistant_settings(hass, CLOUD_ALEXA)
|
self._cur_entity_prefs = async_get_assistant_settings(hass, CLOUD_ALEXA)
|
||||||
self._alexa_sync_unsub: Callable[[], None] | None = None
|
self._alexa_sync_unsub: Callable[[], None] | None = None
|
||||||
self._endpoint = None
|
self._endpoint: Any = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def enabled(self):
|
def enabled(self) -> bool:
|
||||||
"""Return if Alexa is enabled."""
|
"""Return if Alexa is enabled."""
|
||||||
return (
|
return (
|
||||||
self._cloud.is_logged_in
|
self._cloud.is_logged_in
|
||||||
@ -156,12 +161,12 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supports_auth(self):
|
def supports_auth(self) -> bool:
|
||||||
"""Return if config supports auth."""
|
"""Return if config supports auth."""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_report_state(self):
|
def should_report_state(self) -> bool:
|
||||||
"""Return if states should be proactively reported."""
|
"""Return if states should be proactively reported."""
|
||||||
return (
|
return (
|
||||||
self._prefs.alexa_enabled
|
self._prefs.alexa_enabled
|
||||||
@ -170,7 +175,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def endpoint(self):
|
def endpoint(self) -> Any | None:
|
||||||
"""Endpoint for report state."""
|
"""Endpoint for report state."""
|
||||||
if self._endpoint is None:
|
if self._endpoint is None:
|
||||||
raise ValueError("No endpoint available. Fetch access token first")
|
raise ValueError("No endpoint available. Fetch access token first")
|
||||||
@ -178,22 +183,22 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
|||||||
return self._endpoint
|
return self._endpoint
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def locale(self):
|
def locale(self) -> str:
|
||||||
"""Return config locale."""
|
"""Return config locale."""
|
||||||
# Not clear how to determine locale atm.
|
# Not clear how to determine locale atm.
|
||||||
return "en-US"
|
return "en-US"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def entity_config(self):
|
def entity_config(self) -> dict[str, Any]:
|
||||||
"""Return entity config."""
|
"""Return entity config."""
|
||||||
return self._config.get(CONF_ENTITY_CONFIG) or {}
|
return self._config.get(CONF_ENTITY_CONFIG) or {}
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def user_identifier(self):
|
def user_identifier(self) -> str:
|
||||||
"""Return an identifier for the user that represents this config."""
|
"""Return an identifier for the user that represents this config."""
|
||||||
return self._cloud_user
|
return self._cloud_user
|
||||||
|
|
||||||
def _migrate_alexa_entity_settings_v1(self):
|
def _migrate_alexa_entity_settings_v1(self) -> None:
|
||||||
"""Migrate alexa entity settings to entity registry options."""
|
"""Migrate alexa entity settings to entity registry options."""
|
||||||
if not self._config[CONF_FILTER].empty_filter:
|
if not self._config[CONF_FILTER].empty_filter:
|
||||||
# Don't migrate if there's a YAML config
|
# Don't migrate if there's a YAML config
|
||||||
@ -210,11 +215,11 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
|||||||
self._should_expose_legacy(entity_id),
|
self._should_expose_legacy(entity_id),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_initialize(self):
|
async def async_initialize(self) -> None:
|
||||||
"""Initialize the Alexa config."""
|
"""Initialize the Alexa config."""
|
||||||
await super().async_initialize()
|
await super().async_initialize()
|
||||||
|
|
||||||
async def on_hass_started(hass):
|
async def on_hass_started(hass: HomeAssistant) -> None:
|
||||||
if self._prefs.alexa_settings_version != ALEXA_SETTINGS_VERSION:
|
if self._prefs.alexa_settings_version != ALEXA_SETTINGS_VERSION:
|
||||||
if self._prefs.alexa_settings_version < 2 or (
|
if self._prefs.alexa_settings_version < 2 or (
|
||||||
# Recover from a bug we had in 2023.5.0 where entities didn't get exposed
|
# Recover from a bug we had in 2023.5.0 where entities didn't get exposed
|
||||||
@ -235,7 +240,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
|||||||
self.hass, CLOUD_ALEXA, self._async_exposed_entities_updated
|
self.hass, CLOUD_ALEXA, self._async_exposed_entities_updated
|
||||||
)
|
)
|
||||||
|
|
||||||
async def on_hass_start(hass):
|
async def on_hass_start(hass: HomeAssistant) -> None:
|
||||||
if self.enabled and ALEXA_DOMAIN not in self.hass.config.components:
|
if self.enabled and ALEXA_DOMAIN not in self.hass.config.components:
|
||||||
await async_setup_component(self.hass, ALEXA_DOMAIN, {})
|
await async_setup_component(self.hass, ALEXA_DOMAIN, {})
|
||||||
|
|
||||||
@ -248,14 +253,14 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
|||||||
self._handle_entity_registry_updated,
|
self._handle_entity_registry_updated,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _should_expose_legacy(self, entity_id):
|
def _should_expose_legacy(self, entity_id: str) -> bool:
|
||||||
"""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:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
entity_configs = self._prefs.alexa_entity_configs
|
entity_configs = self._prefs.alexa_entity_configs
|
||||||
entity_config = entity_configs.get(entity_id, {})
|
entity_config = entity_configs.get(entity_id, {})
|
||||||
entity_expose = entity_config.get(PREF_SHOULD_EXPOSE)
|
entity_expose: bool | None = entity_config.get(PREF_SHOULD_EXPOSE)
|
||||||
if entity_expose is not None:
|
if entity_expose is not None:
|
||||||
return entity_expose
|
return entity_expose
|
||||||
|
|
||||||
@ -279,21 +284,22 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def should_expose(self, entity_id):
|
def should_expose(self, entity_id: str) -> bool:
|
||||||
"""If an entity should be exposed."""
|
"""If an entity should be exposed."""
|
||||||
if not self._config[CONF_FILTER].empty_filter:
|
entity_filter: EntityFilter = self._config[CONF_FILTER]
|
||||||
|
if not entity_filter.empty_filter:
|
||||||
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
|
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
|
||||||
return False
|
return False
|
||||||
return self._config[CONF_FILTER](entity_id)
|
return entity_filter(entity_id)
|
||||||
|
|
||||||
return async_should_expose(self.hass, CLOUD_ALEXA, entity_id)
|
return async_should_expose(self.hass, CLOUD_ALEXA, entity_id)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_invalidate_access_token(self):
|
def async_invalidate_access_token(self) -> None:
|
||||||
"""Invalidate access token."""
|
"""Invalidate access token."""
|
||||||
self._token_valid = None
|
self._token_valid = None
|
||||||
|
|
||||||
async def async_get_access_token(self):
|
async def async_get_access_token(self) -> Any:
|
||||||
"""Get an access token."""
|
"""Get an access token."""
|
||||||
if self._token_valid is not None and self._token_valid > utcnow():
|
if self._token_valid is not None and self._token_valid > utcnow():
|
||||||
return self._token
|
return self._token
|
||||||
@ -380,7 +386,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
|||||||
self.hass, SYNC_DELAY, self._sync_prefs
|
self.hass, SYNC_DELAY, self._sync_prefs
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _sync_prefs(self, _now):
|
async def _sync_prefs(self, _now: datetime) -> None:
|
||||||
"""Sync the updated preferences to Alexa."""
|
"""Sync the updated preferences to Alexa."""
|
||||||
self._alexa_sync_unsub = None
|
self._alexa_sync_unsub = None
|
||||||
old_prefs = self._cur_entity_prefs
|
old_prefs = self._cur_entity_prefs
|
||||||
@ -432,7 +438,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
|||||||
if await self._sync_helper(to_update, to_remove):
|
if await self._sync_helper(to_update, to_remove):
|
||||||
self._cur_entity_prefs = new_prefs
|
self._cur_entity_prefs = new_prefs
|
||||||
|
|
||||||
async def async_sync_entities(self):
|
async def async_sync_entities(self) -> bool:
|
||||||
"""Sync all entities to Alexa."""
|
"""Sync all entities to Alexa."""
|
||||||
# Remove any pending sync
|
# Remove any pending sync
|
||||||
if self._alexa_sync_unsub:
|
if self._alexa_sync_unsub:
|
||||||
@ -452,7 +458,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
|||||||
|
|
||||||
return await self._sync_helper(to_update, to_remove)
|
return await self._sync_helper(to_update, to_remove)
|
||||||
|
|
||||||
async def _sync_helper(self, to_update, to_remove) -> bool:
|
async def _sync_helper(self, to_update: list[str], to_remove: list[str]) -> bool:
|
||||||
"""Sync entities to Alexa.
|
"""Sync entities to Alexa.
|
||||||
|
|
||||||
Return boolean if it was successful.
|
Return boolean if it was successful.
|
||||||
@ -497,7 +503,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
|||||||
_LOGGER.warning("Error trying to sync entities to Alexa: %s", err)
|
_LOGGER.warning("Error trying to sync entities to Alexa: %s", err)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def _handle_entity_registry_updated(self, event):
|
async def _handle_entity_registry_updated(self, event: Event) -> None:
|
||||||
"""Handle when entity registry updated."""
|
"""Handle when entity registry updated."""
|
||||||
if not self.enabled or not self._cloud.is_logged_in:
|
if not self.enabled or not self._cloud.is_logged_in:
|
||||||
return
|
return
|
||||||
|
@ -2,6 +2,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from collections.abc import Callable
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from hass_nabucasa import Cloud
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
BinarySensorDeviceClass,
|
BinarySensorDeviceClass,
|
||||||
@ -13,6 +17,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
|
||||||
|
from .client import CloudClient
|
||||||
from .const import DISPATCHER_REMOTE_UPDATE, DOMAIN
|
from .const import DISPATCHER_REMOTE_UPDATE, DOMAIN
|
||||||
|
|
||||||
WAIT_UNTIL_CHANGE = 3
|
WAIT_UNTIL_CHANGE = 3
|
||||||
@ -41,10 +46,10 @@ class CloudRemoteBinary(BinarySensorEntity):
|
|||||||
_attr_unique_id = "cloud-remote-ui-connectivity"
|
_attr_unique_id = "cloud-remote-ui-connectivity"
|
||||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||||
|
|
||||||
def __init__(self, cloud):
|
def __init__(self, cloud: Cloud[CloudClient]) -> None:
|
||||||
"""Initialize the binary sensor."""
|
"""Initialize the binary sensor."""
|
||||||
self.cloud = cloud
|
self.cloud = cloud
|
||||||
self._unsub_dispatcher = None
|
self._unsub_dispatcher: Callable[[], None] | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
@ -59,7 +64,7 @@ class CloudRemoteBinary(BinarySensorEntity):
|
|||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Register update dispatcher."""
|
"""Register update dispatcher."""
|
||||||
|
|
||||||
async def async_state_update(data):
|
async def async_state_update(data: Any) -> None:
|
||||||
"""Update callback."""
|
"""Update callback."""
|
||||||
await asyncio.sleep(WAIT_UNTIL_CHANGE)
|
await asyncio.sleep(WAIT_UNTIL_CHANGE)
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from datetime import datetime
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@ -95,7 +96,9 @@ class CloudClient(Interface):
|
|||||||
if self._alexa_config is None:
|
if self._alexa_config is None:
|
||||||
async with self._alexa_config_init_lock:
|
async with self._alexa_config_init_lock:
|
||||||
if self._alexa_config is not None:
|
if self._alexa_config is not None:
|
||||||
return self._alexa_config
|
# This is reachable if the config was set while we waited
|
||||||
|
# for the lock
|
||||||
|
return self._alexa_config # type: ignore[unreachable]
|
||||||
|
|
||||||
cloud_user = await self._prefs.get_cloud_user()
|
cloud_user = await self._prefs.get_cloud_user()
|
||||||
|
|
||||||
@ -136,7 +139,7 @@ class CloudClient(Interface):
|
|||||||
"""When cloud is connected."""
|
"""When cloud is connected."""
|
||||||
is_new_user = await self.prefs.async_set_username(self.cloud.username)
|
is_new_user = await self.prefs.async_set_username(self.cloud.username)
|
||||||
|
|
||||||
async def enable_alexa(_):
|
async def enable_alexa(_: Any) -> None:
|
||||||
"""Enable Alexa."""
|
"""Enable Alexa."""
|
||||||
aconf = await self.get_alexa_config()
|
aconf = await self.get_alexa_config()
|
||||||
try:
|
try:
|
||||||
@ -156,7 +159,7 @@ class CloudClient(Interface):
|
|||||||
|
|
||||||
enable_alexa_job = HassJob(enable_alexa, cancel_on_shutdown=True)
|
enable_alexa_job = HassJob(enable_alexa, cancel_on_shutdown=True)
|
||||||
|
|
||||||
async def enable_google(_):
|
async def enable_google(_: datetime) -> None:
|
||||||
"""Enable Google."""
|
"""Enable Google."""
|
||||||
gconf = await self.get_google_config()
|
gconf = await self.get_google_config()
|
||||||
|
|
||||||
@ -210,7 +213,7 @@ class CloudClient(Interface):
|
|||||||
"""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()
|
aconfig = await self.get_alexa_config()
|
||||||
return await alexa_smart_home.async_handle_message(
|
return await alexa_smart_home.async_handle_message( # type: ignore[no-any-return, no-untyped-call]
|
||||||
self._hass,
|
self._hass,
|
||||||
aconfig,
|
aconfig,
|
||||||
payload,
|
payload,
|
||||||
@ -223,9 +226,11 @@ class CloudClient(Interface):
|
|||||||
gconf = await self.get_google_config()
|
gconf = await self.get_google_config()
|
||||||
|
|
||||||
if not self._prefs.google_enabled:
|
if not self._prefs.google_enabled:
|
||||||
return ga.api_disabled_response(payload, gconf.agent_user_id)
|
return ga.api_disabled_response( # type: ignore[no-any-return, no-untyped-call]
|
||||||
|
payload, gconf.agent_user_id
|
||||||
|
)
|
||||||
|
|
||||||
return await ga.async_handle_message(
|
return await ga.async_handle_message( # type: ignore[no-any-return, no-untyped-call]
|
||||||
self._hass, gconf, gconf.cloud_user, payload, google_assistant.SOURCE_CLOUD
|
self._hass, gconf, gconf.cloud_user, payload, google_assistant.SOURCE_CLOUD
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
"""Google config for Cloud."""
|
"""Google config for Cloud."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from hass_nabucasa import Cloud, cloud_api
|
from hass_nabucasa import Cloud, cloud_api
|
||||||
from hass_nabucasa.google_report_state import ErrorResponse
|
from hass_nabucasa.google_report_state import ErrorResponse
|
||||||
@ -24,12 +26,14 @@ from homeassistant.core import (
|
|||||||
CoreState,
|
CoreState,
|
||||||
Event,
|
Event,
|
||||||
HomeAssistant,
|
HomeAssistant,
|
||||||
|
State,
|
||||||
callback,
|
callback,
|
||||||
split_entity_id,
|
split_entity_id,
|
||||||
)
|
)
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import device_registry as dr, entity_registry as er, start
|
from homeassistant.helpers import device_registry as dr, entity_registry as er, start
|
||||||
from homeassistant.helpers.entity import get_device_class
|
from homeassistant.helpers.entity import get_device_class
|
||||||
|
from homeassistant.helpers.entityfilter import EntityFilter
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
@ -42,6 +46,9 @@ from .const import (
|
|||||||
)
|
)
|
||||||
from .prefs import GOOGLE_SETTINGS_VERSION, CloudPreferences
|
from .prefs import GOOGLE_SETTINGS_VERSION, CloudPreferences
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .client import CloudClient
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CLOUD_GOOGLE = f"{CLOUD_DOMAIN}.{GOOGLE_DOMAIN}"
|
CLOUD_GOOGLE = f"{CLOUD_DOMAIN}.{GOOGLE_DOMAIN}"
|
||||||
@ -123,7 +130,7 @@ class CloudGoogleConfig(AbstractConfig):
|
|||||||
config: dict[str, Any],
|
config: dict[str, Any],
|
||||||
cloud_user: str,
|
cloud_user: str,
|
||||||
prefs: CloudPreferences,
|
prefs: CloudPreferences,
|
||||||
cloud: Cloud,
|
cloud: Cloud[CloudClient],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the Google config."""
|
"""Initialize the Google config."""
|
||||||
super().__init__(hass)
|
super().__init__(hass)
|
||||||
@ -134,7 +141,7 @@ class CloudGoogleConfig(AbstractConfig):
|
|||||||
self._sync_entities_lock = asyncio.Lock()
|
self._sync_entities_lock = asyncio.Lock()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def enabled(self):
|
def enabled(self) -> bool:
|
||||||
"""Return if Google is enabled."""
|
"""Return if Google is enabled."""
|
||||||
return (
|
return (
|
||||||
self._cloud.is_logged_in
|
self._cloud.is_logged_in
|
||||||
@ -143,34 +150,34 @@ class CloudGoogleConfig(AbstractConfig):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def entity_config(self):
|
def entity_config(self) -> dict[str, Any]:
|
||||||
"""Return entity config."""
|
"""Return entity config."""
|
||||||
return self._config.get(CONF_ENTITY_CONFIG) or {}
|
return self._config.get(CONF_ENTITY_CONFIG) or {}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def secure_devices_pin(self):
|
def secure_devices_pin(self) -> str | None:
|
||||||
"""Return entity config."""
|
"""Return entity config."""
|
||||||
return self._prefs.google_secure_devices_pin
|
return self._prefs.google_secure_devices_pin
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_report_state(self):
|
def should_report_state(self) -> bool:
|
||||||
"""Return if states should be proactively reported."""
|
"""Return if states should be proactively reported."""
|
||||||
return self.enabled and self._prefs.google_report_state
|
return self.enabled and self._prefs.google_report_state
|
||||||
|
|
||||||
def get_local_webhook_id(self, agent_user_id):
|
def get_local_webhook_id(self, agent_user_id: Any) -> str:
|
||||||
"""Return the webhook ID to be used for actions for a given agent user id via the local SDK."""
|
"""Return the webhook ID to be used for actions for a given agent user id via the local SDK."""
|
||||||
return self._prefs.google_local_webhook_id
|
return self._prefs.google_local_webhook_id
|
||||||
|
|
||||||
def get_local_agent_user_id(self, webhook_id):
|
def get_local_agent_user_id(self, webhook_id: Any) -> str:
|
||||||
"""Return the user ID to be used for actions received via the local SDK."""
|
"""Return the user ID to be used for actions received via the local SDK."""
|
||||||
return self._user
|
return self._user
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cloud_user(self):
|
def cloud_user(self) -> str:
|
||||||
"""Return Cloud User account."""
|
"""Return Cloud User account."""
|
||||||
return self._user
|
return self._user
|
||||||
|
|
||||||
def _migrate_google_entity_settings_v1(self):
|
def _migrate_google_entity_settings_v1(self) -> None:
|
||||||
"""Migrate Google entity settings to entity registry options."""
|
"""Migrate Google entity settings to entity registry options."""
|
||||||
if not self._config[CONF_FILTER].empty_filter:
|
if not self._config[CONF_FILTER].empty_filter:
|
||||||
# Don't migrate if there's a YAML config
|
# Don't migrate if there's a YAML config
|
||||||
@ -195,7 +202,7 @@ class CloudGoogleConfig(AbstractConfig):
|
|||||||
_2fa_disabled,
|
_2fa_disabled,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_initialize(self):
|
async def async_initialize(self) -> None:
|
||||||
"""Perform async initialization of config."""
|
"""Perform async initialization of config."""
|
||||||
await super().async_initialize()
|
await super().async_initialize()
|
||||||
|
|
||||||
@ -246,18 +253,18 @@ class CloudGoogleConfig(AbstractConfig):
|
|||||||
self._handle_device_registry_updated,
|
self._handle_device_registry_updated,
|
||||||
)
|
)
|
||||||
|
|
||||||
def should_expose(self, state):
|
def should_expose(self, state: State) -> bool:
|
||||||
"""If a state object should be exposed."""
|
"""If a state object should be exposed."""
|
||||||
return self._should_expose_entity_id(state.entity_id)
|
return self._should_expose_entity_id(state.entity_id)
|
||||||
|
|
||||||
def _should_expose_legacy(self, entity_id):
|
def _should_expose_legacy(self, entity_id: str) -> bool:
|
||||||
"""If an entity ID should be exposed."""
|
"""If an entity ID should be exposed."""
|
||||||
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
|
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
entity_configs = self._prefs.google_entity_configs
|
entity_configs = self._prefs.google_entity_configs
|
||||||
entity_config = entity_configs.get(entity_id, {})
|
entity_config = entity_configs.get(entity_id, {})
|
||||||
entity_expose = entity_config.get(PREF_SHOULD_EXPOSE)
|
entity_expose: bool | None = entity_config.get(PREF_SHOULD_EXPOSE)
|
||||||
if entity_expose is not None:
|
if entity_expose is not None:
|
||||||
return entity_expose
|
return entity_expose
|
||||||
|
|
||||||
@ -282,36 +289,37 @@ class CloudGoogleConfig(AbstractConfig):
|
|||||||
and _supported_legacy(self.hass, entity_id)
|
and _supported_legacy(self.hass, entity_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
def _should_expose_entity_id(self, entity_id):
|
def _should_expose_entity_id(self, entity_id: str) -> bool:
|
||||||
"""If an entity should be exposed."""
|
"""If an entity should be exposed."""
|
||||||
if not self._config[CONF_FILTER].empty_filter:
|
entity_filter: EntityFilter = self._config[CONF_FILTER]
|
||||||
|
if not entity_filter.empty_filter:
|
||||||
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
|
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
|
||||||
return False
|
return False
|
||||||
return self._config[CONF_FILTER](entity_id)
|
return entity_filter(entity_id)
|
||||||
|
|
||||||
return async_should_expose(self.hass, CLOUD_GOOGLE, entity_id)
|
return async_should_expose(self.hass, CLOUD_GOOGLE, entity_id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def agent_user_id(self):
|
def agent_user_id(self) -> str:
|
||||||
"""Return Agent User Id to use for query responses."""
|
"""Return Agent User Id to use for query responses."""
|
||||||
return self._cloud.username
|
return self._cloud.username
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_registered_user_agent(self):
|
def has_registered_user_agent(self) -> bool:
|
||||||
"""Return if we have a Agent User Id registered."""
|
"""Return if we have a Agent User Id registered."""
|
||||||
return len(self._store.agent_user_ids) > 0
|
return len(self._store.agent_user_ids) > 0
|
||||||
|
|
||||||
def get_agent_user_id(self, context):
|
def get_agent_user_id(self, context: Any) -> str:
|
||||||
"""Get agent user ID making request."""
|
"""Get agent user ID making request."""
|
||||||
return self.agent_user_id
|
return self.agent_user_id
|
||||||
|
|
||||||
def _2fa_disabled_legacy(self, entity_id):
|
def _2fa_disabled_legacy(self, entity_id: str) -> bool | None:
|
||||||
"""If an entity should be checked for 2FA."""
|
"""If an entity should be checked for 2FA."""
|
||||||
entity_configs = self._prefs.google_entity_configs
|
entity_configs = self._prefs.google_entity_configs
|
||||||
entity_config = entity_configs.get(entity_id, {})
|
entity_config = entity_configs.get(entity_id, {})
|
||||||
return entity_config.get(PREF_DISABLE_2FA)
|
return entity_config.get(PREF_DISABLE_2FA)
|
||||||
|
|
||||||
def should_2fa(self, state):
|
def should_2fa(self, state: State) -> bool:
|
||||||
"""If an entity should be checked for 2FA."""
|
"""If an entity should be checked for 2FA."""
|
||||||
try:
|
try:
|
||||||
settings = async_get_entity_settings(self.hass, state.entity_id)
|
settings = async_get_entity_settings(self.hass, state.entity_id)
|
||||||
@ -322,14 +330,14 @@ class CloudGoogleConfig(AbstractConfig):
|
|||||||
assistant_options = settings.get(CLOUD_GOOGLE, {})
|
assistant_options = settings.get(CLOUD_GOOGLE, {})
|
||||||
return not assistant_options.get(PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA)
|
return not assistant_options.get(PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA)
|
||||||
|
|
||||||
async def async_report_state(self, message, agent_user_id: str):
|
async def async_report_state(self, message: Any, agent_user_id: str) -> None:
|
||||||
"""Send a state report to Google."""
|
"""Send a state report to Google."""
|
||||||
try:
|
try:
|
||||||
await self._cloud.google_report_state.async_send_message(message)
|
await self._cloud.google_report_state.async_send_message(message)
|
||||||
except ErrorResponse as err:
|
except ErrorResponse as err:
|
||||||
_LOGGER.warning("Error reporting state - %s: %s", err.code, err.message)
|
_LOGGER.warning("Error reporting state - %s: %s", err.code, err.message)
|
||||||
|
|
||||||
async def _async_request_sync_devices(self, agent_user_id: str):
|
async def _async_request_sync_devices(self, agent_user_id: str) -> int:
|
||||||
"""Trigger a sync with Google."""
|
"""Trigger a sync with Google."""
|
||||||
if self._sync_entities_lock.locked():
|
if self._sync_entities_lock.locked():
|
||||||
return HTTPStatus.OK
|
return HTTPStatus.OK
|
||||||
@ -338,7 +346,7 @@ class CloudGoogleConfig(AbstractConfig):
|
|||||||
resp = await cloud_api.async_google_actions_request_sync(self._cloud)
|
resp = await cloud_api.async_google_actions_request_sync(self._cloud)
|
||||||
return resp.status
|
return resp.status
|
||||||
|
|
||||||
async def _async_prefs_updated(self, prefs):
|
async def _async_prefs_updated(self, prefs: CloudPreferences) -> None:
|
||||||
"""Handle updated preferences."""
|
"""Handle updated preferences."""
|
||||||
if not self._cloud.is_logged_in:
|
if not self._cloud.is_logged_in:
|
||||||
if self.is_reporting_state:
|
if self.is_reporting_state:
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
"""The HTTP api to control the cloud integration."""
|
"""The HTTP api to control the cloud integration."""
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Mapping
|
from collections.abc import Awaitable, Callable, Coroutine, Mapping
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
import dataclasses
|
import dataclasses
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any, Concatenate, ParamSpec, TypeVar
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
from aiohttp import web
|
||||||
import async_timeout
|
import async_timeout
|
||||||
import attr
|
import attr
|
||||||
from hass_nabucasa import Cloud, auth, thingtalk
|
from hass_nabucasa import Cloud, auth, thingtalk
|
||||||
@ -32,6 +33,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|||||||
from homeassistant.util.location import async_detect_location_info
|
from homeassistant.util.location import async_detect_location_info
|
||||||
|
|
||||||
from .alexa_config import entity_supported as entity_supported_by_alexa
|
from .alexa_config import entity_supported as entity_supported_by_alexa
|
||||||
|
from .client import CloudClient
|
||||||
from .const import (
|
from .const import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
PREF_ALEXA_REPORT_STATE,
|
PREF_ALEXA_REPORT_STATE,
|
||||||
@ -50,7 +52,7 @@ from .subscription import async_subscription_info
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
_CLOUD_ERRORS = {
|
_CLOUD_ERRORS: dict[type[Exception], tuple[HTTPStatus, str]] = {
|
||||||
asyncio.TimeoutError: (
|
asyncio.TimeoutError: (
|
||||||
HTTPStatus.BAD_GATEWAY,
|
HTTPStatus.BAD_GATEWAY,
|
||||||
"Unable to reach the Home Assistant cloud.",
|
"Unable to reach the Home Assistant cloud.",
|
||||||
@ -62,7 +64,7 @@ _CLOUD_ERRORS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass):
|
async def async_setup(hass: HomeAssistant) -> None:
|
||||||
"""Initialize the HTTP API."""
|
"""Initialize the HTTP API."""
|
||||||
websocket_api.async_register_command(hass, websocket_cloud_status)
|
websocket_api.async_register_command(hass, websocket_cloud_status)
|
||||||
websocket_api.async_register_command(hass, websocket_subscription)
|
websocket_api.async_register_command(hass, websocket_subscription)
|
||||||
@ -107,11 +109,21 @@ async def async_setup(hass):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _handle_cloud_errors(handler):
|
_HassViewT = TypeVar("_HassViewT", bound=HomeAssistantView)
|
||||||
|
_P = ParamSpec("_P")
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_cloud_errors(
|
||||||
|
handler: Callable[Concatenate[_HassViewT, web.Request, _P], Awaitable[web.Response]]
|
||||||
|
) -> Callable[
|
||||||
|
Concatenate[_HassViewT, web.Request, _P], Coroutine[Any, Any, web.Response]
|
||||||
|
]:
|
||||||
"""Webview decorator to handle auth errors."""
|
"""Webview decorator to handle auth errors."""
|
||||||
|
|
||||||
@wraps(handler)
|
@wraps(handler)
|
||||||
async def error_handler(view, request, *args, **kwargs):
|
async def error_handler(
|
||||||
|
view: _HassViewT, request: web.Request, *args: _P.args, **kwargs: _P.kwargs
|
||||||
|
) -> web.Response:
|
||||||
"""Handle exceptions that raise from the wrapped request handler."""
|
"""Handle exceptions that raise from the wrapped request handler."""
|
||||||
try:
|
try:
|
||||||
result = await handler(view, request, *args, **kwargs)
|
result = await handler(view, request, *args, **kwargs)
|
||||||
@ -126,25 +138,37 @@ def _handle_cloud_errors(handler):
|
|||||||
return error_handler
|
return error_handler
|
||||||
|
|
||||||
|
|
||||||
def _ws_handle_cloud_errors(handler):
|
def _ws_handle_cloud_errors(
|
||||||
|
handler: Callable[
|
||||||
|
[HomeAssistant, websocket_api.ActiveConnection, dict[str, Any]],
|
||||||
|
Coroutine[None, None, None],
|
||||||
|
]
|
||||||
|
) -> Callable[
|
||||||
|
[HomeAssistant, websocket_api.ActiveConnection, dict[str, Any]],
|
||||||
|
Coroutine[None, None, None],
|
||||||
|
]:
|
||||||
"""Websocket decorator to handle auth errors."""
|
"""Websocket decorator to handle auth errors."""
|
||||||
|
|
||||||
@wraps(handler)
|
@wraps(handler)
|
||||||
async def error_handler(hass, connection, msg):
|
async def error_handler(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
connection: websocket_api.ActiveConnection,
|
||||||
|
msg: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
"""Handle exceptions that raise from the wrapped handler."""
|
"""Handle exceptions that raise from the wrapped handler."""
|
||||||
try:
|
try:
|
||||||
return await handler(hass, connection, msg)
|
return await handler(hass, connection, msg)
|
||||||
|
|
||||||
except Exception as err: # pylint: disable=broad-except
|
except Exception as err: # pylint: disable=broad-except
|
||||||
err_status, err_msg = _process_cloud_exception(err, msg["type"])
|
err_status, err_msg = _process_cloud_exception(err, msg["type"])
|
||||||
connection.send_error(msg["id"], err_status, err_msg)
|
connection.send_error(msg["id"], str(err_status), err_msg)
|
||||||
|
|
||||||
return error_handler
|
return error_handler
|
||||||
|
|
||||||
|
|
||||||
def _process_cloud_exception(exc, where):
|
def _process_cloud_exception(exc: Exception, where: str) -> tuple[HTTPStatus, str]:
|
||||||
"""Process a cloud exception."""
|
"""Process a cloud exception."""
|
||||||
err_info = None
|
err_info: tuple[HTTPStatus, str] | None = None
|
||||||
|
|
||||||
for err, value_info in _CLOUD_ERRORS.items():
|
for err, value_info in _CLOUD_ERRORS.items():
|
||||||
if isinstance(exc, err):
|
if isinstance(exc, err):
|
||||||
@ -165,10 +189,10 @@ class GoogleActionsSyncView(HomeAssistantView):
|
|||||||
name = "api:cloud:google_actions/sync"
|
name = "api:cloud:google_actions/sync"
|
||||||
|
|
||||||
@_handle_cloud_errors
|
@_handle_cloud_errors
|
||||||
async def post(self, request):
|
async def post(self, request: web.Request) -> web.Response:
|
||||||
"""Trigger a Google Actions sync."""
|
"""Trigger a Google Actions sync."""
|
||||||
hass = request.app["hass"]
|
hass = request.app["hass"]
|
||||||
cloud: Cloud = hass.data[DOMAIN]
|
cloud: Cloud[CloudClient] = hass.data[DOMAIN]
|
||||||
gconf = await cloud.client.get_google_config()
|
gconf = await cloud.client.get_google_config()
|
||||||
status = await gconf.async_sync_entities(gconf.agent_user_id)
|
status = await gconf.async_sync_entities(gconf.agent_user_id)
|
||||||
return self.json({}, status_code=status)
|
return self.json({}, status_code=status)
|
||||||
@ -184,7 +208,7 @@ class CloudLoginView(HomeAssistantView):
|
|||||||
@RequestDataValidator(
|
@RequestDataValidator(
|
||||||
vol.Schema({vol.Required("email"): str, vol.Required("password"): str})
|
vol.Schema({vol.Required("email"): str, vol.Required("password"): str})
|
||||||
)
|
)
|
||||||
async def post(self, request, data):
|
async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response:
|
||||||
"""Handle login request."""
|
"""Handle login request."""
|
||||||
|
|
||||||
def cloud_assist_pipeline(hass: HomeAssistant) -> str | None:
|
def cloud_assist_pipeline(hass: HomeAssistant) -> str | None:
|
||||||
@ -221,7 +245,7 @@ class CloudLogoutView(HomeAssistantView):
|
|||||||
name = "api:cloud:logout"
|
name = "api:cloud:logout"
|
||||||
|
|
||||||
@_handle_cloud_errors
|
@_handle_cloud_errors
|
||||||
async def post(self, request):
|
async def post(self, request: web.Request) -> web.Response:
|
||||||
"""Handle logout request."""
|
"""Handle logout request."""
|
||||||
hass = request.app["hass"]
|
hass = request.app["hass"]
|
||||||
cloud = hass.data[DOMAIN]
|
cloud = hass.data[DOMAIN]
|
||||||
@ -247,7 +271,7 @@ class CloudRegisterView(HomeAssistantView):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
async def post(self, request, data):
|
async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response:
|
||||||
"""Handle registration request."""
|
"""Handle registration request."""
|
||||||
hass = request.app["hass"]
|
hass = request.app["hass"]
|
||||||
cloud = hass.data[DOMAIN]
|
cloud = hass.data[DOMAIN]
|
||||||
@ -283,7 +307,7 @@ class CloudResendConfirmView(HomeAssistantView):
|
|||||||
|
|
||||||
@_handle_cloud_errors
|
@_handle_cloud_errors
|
||||||
@RequestDataValidator(vol.Schema({vol.Required("email"): str}))
|
@RequestDataValidator(vol.Schema({vol.Required("email"): str}))
|
||||||
async def post(self, request, data):
|
async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response:
|
||||||
"""Handle resending confirm email code request."""
|
"""Handle resending confirm email code request."""
|
||||||
hass = request.app["hass"]
|
hass = request.app["hass"]
|
||||||
cloud = hass.data[DOMAIN]
|
cloud = hass.data[DOMAIN]
|
||||||
@ -302,7 +326,7 @@ class CloudForgotPasswordView(HomeAssistantView):
|
|||||||
|
|
||||||
@_handle_cloud_errors
|
@_handle_cloud_errors
|
||||||
@RequestDataValidator(vol.Schema({vol.Required("email"): str}))
|
@RequestDataValidator(vol.Schema({vol.Required("email"): str}))
|
||||||
async def post(self, request, data):
|
async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response:
|
||||||
"""Handle forgot password request."""
|
"""Handle forgot password request."""
|
||||||
hass = request.app["hass"]
|
hass = request.app["hass"]
|
||||||
cloud = hass.data[DOMAIN]
|
cloud = hass.data[DOMAIN]
|
||||||
@ -330,11 +354,20 @@ async def websocket_cloud_status(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _require_cloud_login(handler):
|
def _require_cloud_login(
|
||||||
|
handler: Callable[
|
||||||
|
[HomeAssistant, websocket_api.ActiveConnection, dict[str, Any]],
|
||||||
|
None,
|
||||||
|
]
|
||||||
|
) -> Callable[[HomeAssistant, websocket_api.ActiveConnection, dict[str, Any]], None,]:
|
||||||
"""Websocket decorator that requires cloud to be logged in."""
|
"""Websocket decorator that requires cloud to be logged in."""
|
||||||
|
|
||||||
@wraps(handler)
|
@wraps(handler)
|
||||||
def with_cloud_auth(hass, connection, msg):
|
def with_cloud_auth(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
connection: websocket_api.ActiveConnection,
|
||||||
|
msg: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
"""Require to be logged into the cloud."""
|
"""Require to be logged into the cloud."""
|
||||||
cloud = hass.data[DOMAIN]
|
cloud = hass.data[DOMAIN]
|
||||||
if not cloud.is_logged_in:
|
if not cloud.is_logged_in:
|
||||||
@ -467,7 +500,9 @@ async def websocket_hook_delete(
|
|||||||
connection.send_message(websocket_api.result_message(msg["id"]))
|
connection.send_message(websocket_api.result_message(msg["id"]))
|
||||||
|
|
||||||
|
|
||||||
async def _account_data(hass: HomeAssistant, cloud: Cloud):
|
async def _account_data(
|
||||||
|
hass: HomeAssistant, cloud: Cloud[CloudClient]
|
||||||
|
) -> dict[str, Any]:
|
||||||
"""Generate the auth data JSON response."""
|
"""Generate the auth data JSON response."""
|
||||||
|
|
||||||
assert hass.config.api
|
assert hass.config.api
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
"""Preference management for cloud."""
|
"""Preference management for cloud."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable, Coroutine
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from homeassistant.auth.const import GROUP_ID_ADMIN
|
from homeassistant.auth.const import GROUP_ID_ADMIN
|
||||||
from homeassistant.auth.models import User
|
from homeassistant.auth.models import User
|
||||||
from homeassistant.components import webhook
|
from homeassistant.components import webhook
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.storage import Store
|
from homeassistant.helpers.storage import Store
|
||||||
from homeassistant.helpers.typing import UNDEFINED
|
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
|
||||||
from homeassistant.util.logging import async_create_catching_coro
|
from homeassistant.util.logging import async_create_catching_coro
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
@ -63,17 +64,20 @@ class CloudPreferencesStore(Store):
|
|||||||
class CloudPreferences:
|
class CloudPreferences:
|
||||||
"""Handle cloud preferences."""
|
"""Handle cloud preferences."""
|
||||||
|
|
||||||
def __init__(self, hass):
|
_prefs: dict[str, Any]
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant) -> None:
|
||||||
"""Initialize cloud prefs."""
|
"""Initialize cloud prefs."""
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._store = CloudPreferencesStore(
|
self._store = CloudPreferencesStore(
|
||||||
hass, STORAGE_VERSION, STORAGE_KEY, minor_version=STORAGE_VERSION_MINOR
|
hass, STORAGE_VERSION, STORAGE_KEY, minor_version=STORAGE_VERSION_MINOR
|
||||||
)
|
)
|
||||||
self._prefs = None
|
self._listeners: list[
|
||||||
self._listeners = []
|
Callable[[CloudPreferences], Coroutine[Any, Any, None]]
|
||||||
|
] = []
|
||||||
self.last_updated: set[str] = set()
|
self.last_updated: set[str] = set()
|
||||||
|
|
||||||
async def async_initialize(self):
|
async def async_initialize(self) -> None:
|
||||||
"""Finish initializing the preferences."""
|
"""Finish initializing the preferences."""
|
||||||
if (prefs := await self._store.async_load()) is None:
|
if (prefs := await self._store.async_load()) is None:
|
||||||
prefs = self._empty_config("")
|
prefs = self._empty_config("")
|
||||||
@ -89,26 +93,28 @@ class CloudPreferences:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_listen_updates(self, listener):
|
def async_listen_updates(
|
||||||
|
self, listener: Callable[[CloudPreferences], Coroutine[Any, Any, None]]
|
||||||
|
) -> None:
|
||||||
"""Listen for updates to the preferences."""
|
"""Listen for updates to the preferences."""
|
||||||
self._listeners.append(listener)
|
self._listeners.append(listener)
|
||||||
|
|
||||||
async def async_update(
|
async def async_update(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
google_enabled=UNDEFINED,
|
google_enabled: bool | UndefinedType = UNDEFINED,
|
||||||
alexa_enabled=UNDEFINED,
|
alexa_enabled: bool | UndefinedType = UNDEFINED,
|
||||||
remote_enabled=UNDEFINED,
|
remote_enabled: bool | UndefinedType = UNDEFINED,
|
||||||
google_secure_devices_pin=UNDEFINED,
|
google_secure_devices_pin: str | None | UndefinedType = UNDEFINED,
|
||||||
cloudhooks=UNDEFINED,
|
cloudhooks: dict[str, dict[str, str | bool]] | UndefinedType = UNDEFINED,
|
||||||
cloud_user=UNDEFINED,
|
cloud_user: str | UndefinedType = UNDEFINED,
|
||||||
alexa_report_state=UNDEFINED,
|
alexa_report_state: bool | UndefinedType = UNDEFINED,
|
||||||
google_report_state=UNDEFINED,
|
google_report_state: bool | UndefinedType = UNDEFINED,
|
||||||
tts_default_voice=UNDEFINED,
|
tts_default_voice: tuple[str, str] | UndefinedType = UNDEFINED,
|
||||||
remote_domain=UNDEFINED,
|
remote_domain: str | None | UndefinedType = UNDEFINED,
|
||||||
alexa_settings_version=UNDEFINED,
|
alexa_settings_version: int | UndefinedType = UNDEFINED,
|
||||||
google_settings_version=UNDEFINED,
|
google_settings_version: int | UndefinedType = UNDEFINED,
|
||||||
):
|
) -> None:
|
||||||
"""Update user preferences."""
|
"""Update user preferences."""
|
||||||
prefs = {**self._prefs}
|
prefs = {**self._prefs}
|
||||||
|
|
||||||
@ -131,7 +137,7 @@ class CloudPreferences:
|
|||||||
|
|
||||||
await self._save_prefs(prefs)
|
await self._save_prefs(prefs)
|
||||||
|
|
||||||
async def async_set_username(self, username) -> bool:
|
async def async_set_username(self, username: str | None) -> bool:
|
||||||
"""Set the username that is logged in."""
|
"""Set the username that is logged in."""
|
||||||
# Logging out.
|
# Logging out.
|
||||||
if username is None:
|
if username is None:
|
||||||
@ -154,7 +160,7 @@ class CloudPreferences:
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def as_dict(self):
|
def as_dict(self) -> dict[str, Any]:
|
||||||
"""Return dictionary version."""
|
"""Return dictionary version."""
|
||||||
return {
|
return {
|
||||||
PREF_ALEXA_DEFAULT_EXPOSE: self.alexa_default_expose,
|
PREF_ALEXA_DEFAULT_EXPOSE: self.alexa_default_expose,
|
||||||
@ -170,7 +176,7 @@ class CloudPreferences:
|
|||||||
}
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def remote_enabled(self):
|
def remote_enabled(self) -> bool:
|
||||||
"""Return if remote is enabled on start."""
|
"""Return if remote is enabled on start."""
|
||||||
if not self._prefs.get(PREF_ENABLE_REMOTE, False):
|
if not self._prefs.get(PREF_ENABLE_REMOTE, False):
|
||||||
return False
|
return False
|
||||||
@ -178,17 +184,18 @@ class CloudPreferences:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def remote_domain(self):
|
def remote_domain(self) -> str | None:
|
||||||
"""Return remote domain."""
|
"""Return remote domain."""
|
||||||
return self._prefs.get(PREF_REMOTE_DOMAIN)
|
return self._prefs.get(PREF_REMOTE_DOMAIN)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def alexa_enabled(self):
|
def alexa_enabled(self) -> bool:
|
||||||
"""Return if Alexa is enabled."""
|
"""Return if Alexa is enabled."""
|
||||||
return self._prefs[PREF_ENABLE_ALEXA]
|
alexa_enabled: bool = self._prefs[PREF_ENABLE_ALEXA]
|
||||||
|
return alexa_enabled
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def alexa_report_state(self):
|
def alexa_report_state(self) -> bool:
|
||||||
"""Return if Alexa report state is enabled."""
|
"""Return if Alexa report state is enabled."""
|
||||||
return self._prefs.get(PREF_ALEXA_REPORT_STATE, DEFAULT_ALEXA_REPORT_STATE)
|
return self._prefs.get(PREF_ALEXA_REPORT_STATE, DEFAULT_ALEXA_REPORT_STATE)
|
||||||
|
|
||||||
@ -201,44 +208,48 @@ class CloudPreferences:
|
|||||||
return self._prefs.get(PREF_ALEXA_DEFAULT_EXPOSE)
|
return self._prefs.get(PREF_ALEXA_DEFAULT_EXPOSE)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def alexa_entity_configs(self):
|
def alexa_entity_configs(self) -> dict[str, Any]:
|
||||||
"""Return Alexa Entity configurations."""
|
"""Return Alexa Entity configurations."""
|
||||||
return self._prefs.get(PREF_ALEXA_ENTITY_CONFIGS, {})
|
return self._prefs.get(PREF_ALEXA_ENTITY_CONFIGS, {})
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def alexa_settings_version(self):
|
def alexa_settings_version(self) -> int:
|
||||||
"""Return version of Alexa settings."""
|
"""Return version of Alexa settings."""
|
||||||
return self._prefs[PREF_ALEXA_SETTINGS_VERSION]
|
alexa_settings_version: int = self._prefs[PREF_ALEXA_SETTINGS_VERSION]
|
||||||
|
return alexa_settings_version
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def google_enabled(self):
|
def google_enabled(self) -> bool:
|
||||||
"""Return if Google is enabled."""
|
"""Return if Google is enabled."""
|
||||||
return self._prefs[PREF_ENABLE_GOOGLE]
|
google_enabled: bool = self._prefs[PREF_ENABLE_GOOGLE]
|
||||||
|
return google_enabled
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def google_report_state(self):
|
def google_report_state(self) -> bool:
|
||||||
"""Return if Google report state is enabled."""
|
"""Return if Google report state is enabled."""
|
||||||
return self._prefs.get(PREF_GOOGLE_REPORT_STATE, DEFAULT_GOOGLE_REPORT_STATE)
|
return self._prefs.get(PREF_GOOGLE_REPORT_STATE, DEFAULT_GOOGLE_REPORT_STATE)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def google_secure_devices_pin(self):
|
def google_secure_devices_pin(self) -> str | None:
|
||||||
"""Return if Google is allowed to unlock locks."""
|
"""Return if Google is allowed to unlock locks."""
|
||||||
return self._prefs.get(PREF_GOOGLE_SECURE_DEVICES_PIN)
|
return self._prefs.get(PREF_GOOGLE_SECURE_DEVICES_PIN)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def google_entity_configs(self):
|
def google_entity_configs(self) -> dict[str, dict[str, Any]]:
|
||||||
"""Return Google Entity configurations."""
|
"""Return Google Entity configurations."""
|
||||||
return self._prefs.get(PREF_GOOGLE_ENTITY_CONFIGS, {})
|
return self._prefs.get(PREF_GOOGLE_ENTITY_CONFIGS, {})
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def google_settings_version(self):
|
def google_settings_version(self) -> int:
|
||||||
"""Return version of Google settings."""
|
"""Return version of Google settings."""
|
||||||
return self._prefs[PREF_GOOGLE_SETTINGS_VERSION]
|
google_settings_version: int = self._prefs[PREF_GOOGLE_SETTINGS_VERSION]
|
||||||
|
return google_settings_version
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def google_local_webhook_id(self):
|
def google_local_webhook_id(self) -> str:
|
||||||
"""Return Google webhook ID to receive local messages."""
|
"""Return Google webhook ID to receive local messages."""
|
||||||
return self._prefs[PREF_GOOGLE_LOCAL_WEBHOOK_ID]
|
google_local_webhook_id: str = self._prefs[PREF_GOOGLE_LOCAL_WEBHOOK_ID]
|
||||||
|
return google_local_webhook_id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def google_default_expose(self) -> list[str] | None:
|
def google_default_expose(self) -> list[str] | None:
|
||||||
@ -249,12 +260,12 @@ class CloudPreferences:
|
|||||||
return self._prefs.get(PREF_GOOGLE_DEFAULT_EXPOSE)
|
return self._prefs.get(PREF_GOOGLE_DEFAULT_EXPOSE)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cloudhooks(self):
|
def cloudhooks(self) -> dict[str, Any]:
|
||||||
"""Return the published cloud webhooks."""
|
"""Return the published cloud webhooks."""
|
||||||
return self._prefs.get(PREF_CLOUDHOOKS, {})
|
return self._prefs.get(PREF_CLOUDHOOKS, {})
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tts_default_voice(self):
|
def tts_default_voice(self) -> tuple[str, str]:
|
||||||
"""Return the default TTS voice."""
|
"""Return the default TTS voice."""
|
||||||
return self._prefs.get(PREF_TTS_DEFAULT_VOICE, DEFAULT_TTS_DEFAULT_VOICE)
|
return self._prefs.get(PREF_TTS_DEFAULT_VOICE, DEFAULT_TTS_DEFAULT_VOICE)
|
||||||
|
|
||||||
@ -281,7 +292,7 @@ class CloudPreferences:
|
|||||||
# an image was restored without restoring the cloud prefs.
|
# an image was restored without restoring the cloud prefs.
|
||||||
return await self._hass.auth.async_get_user(user_id)
|
return await self._hass.auth.async_get_user(user_id)
|
||||||
|
|
||||||
async def _save_prefs(self, prefs):
|
async def _save_prefs(self, prefs: dict[str, Any]) -> None:
|
||||||
"""Save preferences to disk."""
|
"""Save preferences to disk."""
|
||||||
self.last_updated = {
|
self.last_updated = {
|
||||||
key for key, value in prefs.items() if value != self._prefs.get(key)
|
key for key, value in prefs.items() if value != self._prefs.get(key)
|
||||||
@ -294,7 +305,7 @@ class CloudPreferences:
|
|||||||
|
|
||||||
@callback
|
@callback
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _empty_config(username):
|
def _empty_config(username: str) -> dict[str, Any]:
|
||||||
"""Return an empty config."""
|
"""Return an empty config."""
|
||||||
return {
|
return {
|
||||||
PREF_ALEXA_DEFAULT_EXPOSE: DEFAULT_EXPOSED_DOMAINS,
|
PREF_ALEXA_DEFAULT_EXPOSE: DEFAULT_EXPOSED_DOMAINS,
|
||||||
|
@ -12,6 +12,7 @@ from homeassistant.core import HomeAssistant, callback
|
|||||||
from homeassistant.data_entry_flow import FlowResult
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
from homeassistant.helpers import issue_registry as ir
|
from homeassistant.helpers import issue_registry as ir
|
||||||
|
|
||||||
|
from .client import CloudClient
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .subscription import async_migrate_paypal_agreement, async_subscription_info
|
from .subscription import async_migrate_paypal_agreement, async_subscription_info
|
||||||
|
|
||||||
@ -67,7 +68,7 @@ class LegacySubscriptionRepairFlow(RepairsFlow):
|
|||||||
async def async_step_change_plan(self, _: None = None) -> FlowResult:
|
async def async_step_change_plan(self, _: None = None) -> FlowResult:
|
||||||
"""Wait for the user to authorize the app installation."""
|
"""Wait for the user to authorize the app installation."""
|
||||||
|
|
||||||
cloud: Cloud = self.hass.data[DOMAIN]
|
cloud: Cloud[CloudClient] = self.hass.data[DOMAIN]
|
||||||
|
|
||||||
async def _async_wait_for_plan_change() -> None:
|
async def _async_wait_for_plan_change() -> None:
|
||||||
flow_manager = repairs_flow_manager(self.hass)
|
flow_manager = repairs_flow_manager(self.hass)
|
||||||
|
@ -18,15 +18,22 @@ from homeassistant.components.stt import (
|
|||||||
SpeechResult,
|
SpeechResult,
|
||||||
SpeechResultState,
|
SpeechResultState,
|
||||||
)
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
|
||||||
|
from .client import CloudClient
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def async_get_engine(hass, config, discovery_info=None):
|
async def async_get_engine(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config: ConfigType,
|
||||||
|
discovery_info: DiscoveryInfoType | None = None,
|
||||||
|
) -> CloudProvider:
|
||||||
"""Set up Cloud speech component."""
|
"""Set up Cloud speech component."""
|
||||||
cloud: Cloud = hass.data[DOMAIN]
|
cloud: Cloud[CloudClient] = hass.data[DOMAIN]
|
||||||
|
|
||||||
cloud_provider = CloudProvider(cloud)
|
cloud_provider = CloudProvider(cloud)
|
||||||
if discovery_info is not None:
|
if discovery_info is not None:
|
||||||
@ -37,7 +44,7 @@ async def async_get_engine(hass, config, discovery_info=None):
|
|||||||
class CloudProvider(Provider):
|
class CloudProvider(Provider):
|
||||||
"""NabuCasa speech API provider."""
|
"""NabuCasa speech API provider."""
|
||||||
|
|
||||||
def __init__(self, cloud: Cloud) -> None:
|
def __init__(self, cloud: Cloud[CloudClient]) -> None:
|
||||||
"""Home Assistant NabuCasa Speech to text."""
|
"""Home Assistant NabuCasa Speech to text."""
|
||||||
self.cloud = cloud
|
self.cloud = cloud
|
||||||
|
|
||||||
|
@ -9,12 +9,13 @@ from aiohttp.client_exceptions import ClientError
|
|||||||
import async_timeout
|
import async_timeout
|
||||||
from hass_nabucasa import Cloud, cloud_api
|
from hass_nabucasa import Cloud, cloud_api
|
||||||
|
|
||||||
|
from .client import CloudClient
|
||||||
from .const import REQUEST_TIMEOUT
|
from .const import REQUEST_TIMEOUT
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def async_subscription_info(cloud: Cloud) -> dict[str, Any] | None:
|
async def async_subscription_info(cloud: Cloud[CloudClient]) -> dict[str, Any] | None:
|
||||||
"""Fetch the subscription info."""
|
"""Fetch the subscription info."""
|
||||||
try:
|
try:
|
||||||
async with async_timeout.timeout(REQUEST_TIMEOUT):
|
async with async_timeout.timeout(REQUEST_TIMEOUT):
|
||||||
@ -33,7 +34,9 @@ async def async_subscription_info(cloud: Cloud) -> dict[str, Any] | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
async def async_migrate_paypal_agreement(cloud: Cloud) -> dict[str, Any] | None:
|
async def async_migrate_paypal_agreement(
|
||||||
|
cloud: Cloud[CloudClient],
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
"""Migrate a paypal agreement from legacy."""
|
"""Migrate a paypal agreement from legacy."""
|
||||||
try:
|
try:
|
||||||
async with async_timeout.timeout(REQUEST_TIMEOUT):
|
async with async_timeout.timeout(REQUEST_TIMEOUT):
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
"""Provide info to system health."""
|
"""Provide info to system health."""
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from hass_nabucasa import Cloud
|
from hass_nabucasa import Cloud
|
||||||
|
|
||||||
from homeassistant.components import system_health
|
from homeassistant.components import system_health
|
||||||
@ -16,12 +18,12 @@ def async_register(
|
|||||||
register.async_register_info(system_health_info, "/config/cloud")
|
register.async_register_info(system_health_info, "/config/cloud")
|
||||||
|
|
||||||
|
|
||||||
async def system_health_info(hass):
|
async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
|
||||||
"""Get info for the info page."""
|
"""Get info for the info page."""
|
||||||
cloud: Cloud = hass.data[DOMAIN]
|
cloud: Cloud[CloudClient] = hass.data[DOMAIN]
|
||||||
client: CloudClient = cloud.client
|
client = cloud.client
|
||||||
|
|
||||||
data = {
|
data: dict[str, Any] = {
|
||||||
"logged_in": cloud.is_logged_in,
|
"logged_in": cloud.is_logged_in,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
"""Support for the cloud for text to speech service."""
|
"""Support for the cloud for text to speech service."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from hass_nabucasa import Cloud
|
from hass_nabucasa import Cloud
|
||||||
from hass_nabucasa.voice import MAP_VOICE, TTS_VOICES, AudioOutput, VoiceError
|
from hass_nabucasa.voice import MAP_VOICE, TTS_VOICES, AudioOutput, VoiceError
|
||||||
@ -12,11 +14,15 @@ from homeassistant.components.tts import (
|
|||||||
CONF_LANG,
|
CONF_LANG,
|
||||||
PLATFORM_SCHEMA,
|
PLATFORM_SCHEMA,
|
||||||
Provider,
|
Provider,
|
||||||
|
TtsAudioType,
|
||||||
Voice,
|
Voice,
|
||||||
)
|
)
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
|
||||||
|
from .client import CloudClient
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
from .prefs import CloudPreferences
|
||||||
|
|
||||||
ATTR_GENDER = "gender"
|
ATTR_GENDER = "gender"
|
||||||
|
|
||||||
@ -25,7 +31,7 @@ SUPPORT_LANGUAGES = list(TTS_VOICES)
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def validate_lang(value):
|
def validate_lang(value: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""Validate chosen gender or language."""
|
"""Validate chosen gender or language."""
|
||||||
if (lang := value.get(CONF_LANG)) is None:
|
if (lang := value.get(CONF_LANG)) is None:
|
||||||
return value
|
return value
|
||||||
@ -52,10 +58,16 @@ PLATFORM_SCHEMA = vol.All(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_get_engine(hass, config, discovery_info=None):
|
async def async_get_engine(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config: ConfigType,
|
||||||
|
discovery_info: DiscoveryInfoType | None = None,
|
||||||
|
) -> CloudProvider:
|
||||||
"""Set up Cloud speech component."""
|
"""Set up Cloud speech component."""
|
||||||
cloud: Cloud = hass.data[DOMAIN]
|
cloud: Cloud[CloudClient] = hass.data[DOMAIN]
|
||||||
|
|
||||||
|
language: str | None
|
||||||
|
gender: str | None
|
||||||
if discovery_info is not None:
|
if discovery_info is not None:
|
||||||
language = None
|
language = None
|
||||||
gender = None
|
gender = None
|
||||||
@ -72,7 +84,9 @@ async def async_get_engine(hass, config, discovery_info=None):
|
|||||||
class CloudProvider(Provider):
|
class CloudProvider(Provider):
|
||||||
"""NabuCasa Cloud speech API provider."""
|
"""NabuCasa Cloud speech API provider."""
|
||||||
|
|
||||||
def __init__(self, cloud: Cloud, language: str, gender: str) -> None:
|
def __init__(
|
||||||
|
self, cloud: Cloud[CloudClient], language: str | None, gender: str | None
|
||||||
|
) -> None:
|
||||||
"""Initialize cloud provider."""
|
"""Initialize cloud provider."""
|
||||||
self.cloud = cloud
|
self.cloud = cloud
|
||||||
self.name = "Cloud"
|
self.name = "Cloud"
|
||||||
@ -85,22 +99,22 @@ class CloudProvider(Provider):
|
|||||||
self._language, self._gender = cloud.client.prefs.tts_default_voice
|
self._language, self._gender = cloud.client.prefs.tts_default_voice
|
||||||
cloud.client.prefs.async_listen_updates(self._sync_prefs)
|
cloud.client.prefs.async_listen_updates(self._sync_prefs)
|
||||||
|
|
||||||
async def _sync_prefs(self, prefs):
|
async def _sync_prefs(self, prefs: CloudPreferences) -> None:
|
||||||
"""Sync preferences."""
|
"""Sync preferences."""
|
||||||
self._language, self._gender = prefs.tts_default_voice
|
self._language, self._gender = prefs.tts_default_voice
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def default_language(self):
|
def default_language(self) -> str | None:
|
||||||
"""Return the default language."""
|
"""Return the default language."""
|
||||||
return self._language
|
return self._language
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_languages(self):
|
def supported_languages(self) -> list[str]:
|
||||||
"""Return list of supported languages."""
|
"""Return list of supported languages."""
|
||||||
return SUPPORT_LANGUAGES
|
return SUPPORT_LANGUAGES
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_options(self):
|
def supported_options(self) -> list[str]:
|
||||||
"""Return list of supported options like voice, emotion."""
|
"""Return list of supported options like voice, emotion."""
|
||||||
return [ATTR_GENDER, ATTR_VOICE, ATTR_AUDIO_OUTPUT]
|
return [ATTR_GENDER, ATTR_VOICE, ATTR_AUDIO_OUTPUT]
|
||||||
|
|
||||||
@ -112,17 +126,20 @@ class CloudProvider(Provider):
|
|||||||
return [Voice(voice, voice) for voice in voices]
|
return [Voice(voice, voice) for voice in voices]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def default_options(self):
|
def default_options(self) -> dict[str, Any]:
|
||||||
"""Return a dict include default options."""
|
"""Return a dict include default options."""
|
||||||
return {
|
return {
|
||||||
ATTR_GENDER: self._gender,
|
ATTR_GENDER: self._gender,
|
||||||
ATTR_AUDIO_OUTPUT: AudioOutput.MP3,
|
ATTR_AUDIO_OUTPUT: AudioOutput.MP3,
|
||||||
}
|
}
|
||||||
|
|
||||||
async def async_get_tts_audio(self, message, language, options=None):
|
async def async_get_tts_audio(
|
||||||
|
self, message: str, language: str, options: dict[str, Any] | None = None
|
||||||
|
) -> TtsAudioType:
|
||||||
"""Load TTS from NabuCasa Cloud."""
|
"""Load TTS from NabuCasa Cloud."""
|
||||||
# Process TTS
|
# Process TTS
|
||||||
try:
|
try:
|
||||||
|
assert options is not None
|
||||||
data = await self.cloud.voice.process_tts(
|
data = await self.cloud.voice.process_tts(
|
||||||
text=message,
|
text=message,
|
||||||
language=language,
|
language=language,
|
||||||
|
@ -21,7 +21,7 @@ from homeassistant.const import (
|
|||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
STATE_UNAVAILABLE,
|
STATE_UNAVAILABLE,
|
||||||
)
|
)
|
||||||
from homeassistant.core import Context, HomeAssistant, State, callback
|
from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant, State, callback
|
||||||
from homeassistant.helpers import (
|
from homeassistant.helpers import (
|
||||||
area_registry as ar,
|
area_registry as ar,
|
||||||
device_registry as dr,
|
device_registry as dr,
|
||||||
@ -86,19 +86,19 @@ def _get_registry_entries(
|
|||||||
class AbstractConfig(ABC):
|
class AbstractConfig(ABC):
|
||||||
"""Hold the configuration for Google Assistant."""
|
"""Hold the configuration for Google Assistant."""
|
||||||
|
|
||||||
|
_store: GoogleConfigStore
|
||||||
_unsub_report_state: Callable[[], None] | None = None
|
_unsub_report_state: Callable[[], None] | None = None
|
||||||
|
|
||||||
def __init__(self, hass):
|
def __init__(self, hass: HomeAssistant) -> None:
|
||||||
"""Initialize abstract config."""
|
"""Initialize abstract config."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self._store = None
|
self._google_sync_unsub: dict[str, CALLBACK_TYPE] = {}
|
||||||
self._google_sync_unsub = {}
|
|
||||||
self._local_sdk_active = False
|
self._local_sdk_active = False
|
||||||
self._local_last_active: datetime | None = None
|
self._local_last_active: datetime | None = None
|
||||||
self._local_sdk_version_warn = False
|
self._local_sdk_version_warn = False
|
||||||
self.is_supported_cache: dict[str, tuple[int | None, bool]] = {}
|
self.is_supported_cache: dict[str, tuple[int | None, bool]] = {}
|
||||||
|
|
||||||
async def async_initialize(self):
|
async def async_initialize(self) -> None:
|
||||||
"""Perform async initialization of config."""
|
"""Perform async initialization of config."""
|
||||||
self._store = GoogleConfigStore(self.hass)
|
self._store = GoogleConfigStore(self.hass)
|
||||||
await self._store.async_initialize()
|
await self._store.async_initialize()
|
||||||
@ -195,7 +195,7 @@ class AbstractConfig(ABC):
|
|||||||
await gather(*jobs)
|
await gather(*jobs)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_enable_report_state(self):
|
def async_enable_report_state(self) -> None:
|
||||||
"""Enable proactive mode."""
|
"""Enable proactive mode."""
|
||||||
# Circular dep
|
# Circular dep
|
||||||
# pylint: disable-next=import-outside-toplevel
|
# pylint: disable-next=import-outside-toplevel
|
||||||
@ -205,7 +205,7 @@ class AbstractConfig(ABC):
|
|||||||
self._unsub_report_state = async_enable_report_state(self.hass, self)
|
self._unsub_report_state = async_enable_report_state(self.hass, self)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_disable_report_state(self):
|
def async_disable_report_state(self) -> None:
|
||||||
"""Disable report state."""
|
"""Disable report state."""
|
||||||
if self._unsub_report_state is not None:
|
if self._unsub_report_state is not None:
|
||||||
self._unsub_report_state()
|
self._unsub_report_state()
|
||||||
@ -220,7 +220,7 @@ class AbstractConfig(ABC):
|
|||||||
await self.async_disconnect_agent_user(agent_user_id)
|
await self.async_disconnect_agent_user(agent_user_id)
|
||||||
return status
|
return status
|
||||||
|
|
||||||
async def async_sync_entities_all(self):
|
async def async_sync_entities_all(self) -> int:
|
||||||
"""Sync all entities to Google for all registered agents."""
|
"""Sync all entities to Google for all registered agents."""
|
||||||
if not self._store.agent_user_ids:
|
if not self._store.agent_user_ids:
|
||||||
return 204
|
return 204
|
||||||
@ -249,7 +249,7 @@ class AbstractConfig(ABC):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_schedule_google_sync_all(self):
|
def async_schedule_google_sync_all(self) -> None:
|
||||||
"""Schedule a sync for all registered agents."""
|
"""Schedule a sync for all registered agents."""
|
||||||
for agent_user_id in self._store.agent_user_ids:
|
for agent_user_id in self._store.agent_user_ids:
|
||||||
self.async_schedule_google_sync(agent_user_id)
|
self.async_schedule_google_sync(agent_user_id)
|
||||||
@ -279,7 +279,7 @@ class AbstractConfig(ABC):
|
|||||||
self._store.pop_agent_user_id(agent_user_id)
|
self._store.pop_agent_user_id(agent_user_id)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_enable_local_sdk(self):
|
def async_enable_local_sdk(self) -> None:
|
||||||
"""Enable the local SDK."""
|
"""Enable the local SDK."""
|
||||||
setup_successful = True
|
setup_successful = True
|
||||||
setup_webhook_ids = []
|
setup_webhook_ids = []
|
||||||
@ -323,7 +323,7 @@ class AbstractConfig(ABC):
|
|||||||
self._local_sdk_active = setup_successful
|
self._local_sdk_active = setup_successful
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_disable_local_sdk(self):
|
def async_disable_local_sdk(self) -> None:
|
||||||
"""Disable the local SDK."""
|
"""Disable the local SDK."""
|
||||||
if not self._local_sdk_active:
|
if not self._local_sdk_active:
|
||||||
return
|
return
|
||||||
@ -500,7 +500,7 @@ class GoogleEntity:
|
|||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.config = config
|
self.config = config
|
||||||
self.state = state
|
self.state = state
|
||||||
self._traits = None
|
self._traits: list[trait._Trait] | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def entity_id(self):
|
def entity_id(self):
|
||||||
@ -508,7 +508,7 @@ class GoogleEntity:
|
|||||||
return self.state.entity_id
|
return self.state.entity_id
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def traits(self):
|
def traits(self) -> list[trait._Trait]:
|
||||||
"""Return traits for entity."""
|
"""Return traits for entity."""
|
||||||
if self._traits is not None:
|
if self._traits is not None:
|
||||||
return self._traits
|
return self._traits
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""Implement the Google Smart Home traits."""
|
"""Implement the Google Smart Home traits."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, TypeVar
|
from typing import Any, TypeVar
|
||||||
|
|
||||||
@ -196,9 +197,10 @@ def _next_selected(items: list[str], selected: str | None) -> str | None:
|
|||||||
return items[next_item]
|
return items[next_item]
|
||||||
|
|
||||||
|
|
||||||
class _Trait:
|
class _Trait(ABC):
|
||||||
"""Represents a Trait inside Google Assistant skill."""
|
"""Represents a Trait inside Google Assistant skill."""
|
||||||
|
|
||||||
|
name: str
|
||||||
commands: list[str] = []
|
commands: list[str] = []
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -206,6 +208,11 @@ class _Trait:
|
|||||||
"""Return if the trait might ask for 2FA."""
|
"""Return if the trait might ask for 2FA."""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@abstractmethod
|
||||||
|
def supported(domain, features, device_class, attributes):
|
||||||
|
"""Test if state is supported."""
|
||||||
|
|
||||||
def __init__(self, hass, state, config):
|
def __init__(self, hass, state, config):
|
||||||
"""Initialize a trait for a state."""
|
"""Initialize a trait for a state."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
|
10
mypy.ini
10
mypy.ini
@ -631,6 +631,16 @@ disallow_untyped_defs = true
|
|||||||
warn_return_any = true
|
warn_return_any = true
|
||||||
warn_unreachable = true
|
warn_unreachable = true
|
||||||
|
|
||||||
|
[mypy-homeassistant.components.cloud.*]
|
||||||
|
check_untyped_defs = true
|
||||||
|
disallow_incomplete_defs = true
|
||||||
|
disallow_subclassing_any = true
|
||||||
|
disallow_untyped_calls = true
|
||||||
|
disallow_untyped_decorators = true
|
||||||
|
disallow_untyped_defs = true
|
||||||
|
warn_return_any = true
|
||||||
|
warn_unreachable = true
|
||||||
|
|
||||||
[mypy-homeassistant.components.configurator.*]
|
[mypy-homeassistant.components.configurator.*]
|
||||||
check_untyped_defs = true
|
check_untyped_defs = true
|
||||||
disallow_incomplete_defs = true
|
disallow_incomplete_defs = true
|
||||||
|
@ -135,7 +135,7 @@ async def test_setup_existing_cloud_user(
|
|||||||
|
|
||||||
async def test_on_connect(hass: HomeAssistant, mock_cloud_fixture) -> None:
|
async def test_on_connect(hass: HomeAssistant, mock_cloud_fixture) -> None:
|
||||||
"""Test cloud on connect triggers."""
|
"""Test cloud on connect triggers."""
|
||||||
cl: Cloud = hass.data["cloud"]
|
cl: Cloud[cloud.client.CloudClient] = hass.data["cloud"]
|
||||||
|
|
||||||
assert len(cl.iot._on_connect) == 4
|
assert len(cl.iot._on_connect) == 4
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user