mirror of
https://github.com/home-assistant/core.git
synced 2025-06-25 01:21:51 +02: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:
@ -87,6 +87,7 @@ homeassistant.components.camera.*
|
||||
homeassistant.components.canary.*
|
||||
homeassistant.components.clickatell.*
|
||||
homeassistant.components.clicksend.*
|
||||
homeassistant.components.cloud.*
|
||||
homeassistant.components.configurator.*
|
||||
homeassistant.components.cover.*
|
||||
homeassistant.components.cpuspeed.*
|
||||
|
@ -1,4 +1,6 @@
|
||||
"""Config helpers for Alexa."""
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
import asyncio
|
||||
import logging
|
||||
@ -17,15 +19,15 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class AbstractConfig(ABC):
|
||||
"""Hold the configuration for Alexa."""
|
||||
|
||||
_store: AlexaConfigStore
|
||||
_unsub_proactive_report: CALLBACK_TYPE | None = None
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize abstract config."""
|
||||
self.hass = hass
|
||||
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."""
|
||||
self._store = AlexaConfigStore(self.hass)
|
||||
await self._store.async_load()
|
||||
@ -65,7 +67,7 @@ class AbstractConfig(ABC):
|
||||
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) -> None:
|
||||
"""Enable proactive mode."""
|
||||
_LOGGER.debug("Enable proactive mode")
|
||||
async with self._enable_proactive_mode_lock:
|
||||
@ -75,7 +77,7 @@ class AbstractConfig(ABC):
|
||||
self.hass, self
|
||||
)
|
||||
|
||||
async def async_disable_proactive_mode(self):
|
||||
async def async_disable_proactive_mode(self) -> None:
|
||||
"""Disable proactive mode."""
|
||||
_LOGGER.debug("Disable proactive mode")
|
||||
if unsub_func := self._unsub_proactive_report:
|
||||
@ -105,7 +107,7 @@ class AbstractConfig(ABC):
|
||||
"""Return authorization status."""
|
||||
return self._store.authorized
|
||||
|
||||
async def set_authorized(self, authorized):
|
||||
async def set_authorized(self, authorized) -> None:
|
||||
"""Set authorization status.
|
||||
|
||||
- Set when an incoming message is received from Alexa.
|
||||
|
@ -5,7 +5,7 @@ import asyncio
|
||||
from http import HTTPStatus
|
||||
import json
|
||||
import logging
|
||||
from typing import cast
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
@ -23,6 +23,9 @@ from .entities import ENTITY_ADAPTERS, AlexaEntity, generate_alexa_id
|
||||
from .errors import NoTokenAvailable, RequireRelink
|
||||
from .messages import AlexaResponse
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .config import AbstractConfig
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-discovery.html#deletereport-event
|
||||
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable, Callable
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from enum import Enum
|
||||
|
||||
from hass_nabucasa import Cloud
|
||||
@ -18,7 +18,7 @@ from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
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.helpers import config_validation as cv, entityfilter
|
||||
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.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.aiohttp import MockRequest
|
||||
|
||||
from . import account_link, http_api
|
||||
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):
|
||||
raise CloudNotAvailable
|
||||
|
||||
hook = await hass.data[DOMAIN].cloudhooks.async_create(webhook_id, True)
|
||||
return hook["cloudhook_url"]
|
||||
cloud: Cloud[CloudClient] = hass.data[DOMAIN]
|
||||
hook = await cloud.cloudhooks.async_create(webhook_id, True)
|
||||
cloudhook_url: str = hook["cloudhook_url"]
|
||||
return cloudhook_url
|
||||
|
||||
|
||||
@bind_hass
|
||||
@ -213,14 +214,6 @@ def async_remote_ui_url(hass: HomeAssistant) -> str:
|
||||
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:
|
||||
"""Initialize the Home Assistant cloud."""
|
||||
# Process configs
|
||||
@ -243,7 +236,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
cloud = hass.data[DOMAIN] = Cloud(client, **kwargs)
|
||||
cloud.iot.register_on_connect(client.on_cloud_connected)
|
||||
|
||||
async def _shutdown(event):
|
||||
async def _shutdown(event: Event) -> None:
|
||||
"""Shutdown event."""
|
||||
await cloud.stop()
|
||||
|
||||
@ -263,7 +256,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
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."""
|
||||
if not cloud.is_logged_in:
|
||||
return
|
||||
@ -273,7 +266,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
loaded = False
|
||||
|
||||
async def _on_start():
|
||||
async def _on_start() -> None:
|
||||
"""Discover platforms."""
|
||||
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 asyncio.gather(stt_platform_loaded.wait(), tts_platform_loaded.wait())
|
||||
|
||||
async def _on_connect():
|
||||
async def _on_connect() -> None:
|
||||
"""Handle cloud connect."""
|
||||
async_dispatcher_send(
|
||||
hass, SIGNAL_CLOUD_CONNECTION_STATE, CloudConnectionState.CLOUD_CONNECTED
|
||||
)
|
||||
|
||||
async def _on_disconnect():
|
||||
async def _on_disconnect() -> None:
|
||||
"""Handle cloud disconnect."""
|
||||
async_dispatcher_send(
|
||||
hass, SIGNAL_CLOUD_CONNECTION_STATE, CloudConnectionState.CLOUD_DISCONNECTED
|
||||
)
|
||||
|
||||
async def _on_initialized():
|
||||
async def _on_initialized() -> None:
|
||||
"""Update preferences."""
|
||||
await prefs.async_update(remote_domain=cloud.remote.instance_domain)
|
||||
|
||||
@ -330,7 +323,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
|
||||
@callback
|
||||
def _remote_handle_prefs_updated(cloud: Cloud) -> None:
|
||||
def _remote_handle_prefs_updated(cloud: Cloud[CloudClient]) -> None:
|
||||
"""Handle remote preferences updated."""
|
||||
cur_pref = cloud.client.prefs.remote_enabled
|
||||
lock = asyncio.Lock()
|
||||
|
@ -1,5 +1,8 @@
|
||||
"""Account linking via the cloud."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@ -24,14 +27,16 @@ CURRENT_PLAIN_VERSION = AwesomeVersion(
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup(hass: HomeAssistant):
|
||||
def async_setup(hass: HomeAssistant) -> None:
|
||||
"""Set up cloud account link."""
|
||||
config_entry_oauth2_flow.async_add_implementation_provider(
|
||||
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."""
|
||||
services = await _get_services(hass)
|
||||
|
||||
@ -55,9 +60,11 @@ async def async_provide_implementation(hass: HomeAssistant, domain: str):
|
||||
return []
|
||||
|
||||
|
||||
async def _get_services(hass):
|
||||
async def _get_services(hass: HomeAssistant) -> list[dict[str, Any]]:
|
||||
"""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
|
||||
|
||||
try:
|
||||
@ -68,7 +75,7 @@ async def _get_services(hass):
|
||||
hass.data[DATA_SERVICES] = services
|
||||
|
||||
@callback
|
||||
def clear_services(_now):
|
||||
def clear_services(_now: datetime) -> None:
|
||||
"""Clear services cache."""
|
||||
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()
|
||||
|
||||
async def await_tokens():
|
||||
async def await_tokens() -> None:
|
||||
"""Wait for tokens and pass them on when received."""
|
||||
try:
|
||||
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:
|
||||
"""Resolve external data to 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:
|
||||
"""Refresh a token."""
|
||||
|
@ -4,9 +4,10 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from contextlib import suppress
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
@ -29,10 +30,11 @@ from homeassistant.components.homeassistant.exposed_entities import (
|
||||
)
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
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.helpers import entity_registry as er, start
|
||||
from homeassistant.helpers.entity import get_device_class
|
||||
from homeassistant.helpers.entityfilter import EntityFilter
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util.dt import utcnow
|
||||
@ -47,6 +49,9 @@ from .const import (
|
||||
)
|
||||
from .prefs import ALEXA_SETTINGS_VERSION, CloudPreferences
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .client import CloudClient
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CLOUD_ALEXA = f"{CLOUD_DOMAIN}.{ALEXA_DOMAIN}"
|
||||
@ -132,7 +137,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
||||
config: dict,
|
||||
cloud_user: str,
|
||||
prefs: CloudPreferences,
|
||||
cloud: Cloud,
|
||||
cloud: Cloud[CloudClient],
|
||||
) -> None:
|
||||
"""Initialize the Alexa config."""
|
||||
super().__init__(hass)
|
||||
@ -141,13 +146,13 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
||||
self._prefs = prefs
|
||||
self._cloud = cloud
|
||||
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._alexa_sync_unsub: Callable[[], None] | None = None
|
||||
self._endpoint = None
|
||||
self._endpoint: Any = None
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
def enabled(self) -> bool:
|
||||
"""Return if Alexa is enabled."""
|
||||
return (
|
||||
self._cloud.is_logged_in
|
||||
@ -156,12 +161,12 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
||||
)
|
||||
|
||||
@property
|
||||
def supports_auth(self):
|
||||
def supports_auth(self) -> bool:
|
||||
"""Return if config supports auth."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def should_report_state(self):
|
||||
def should_report_state(self) -> bool:
|
||||
"""Return if states should be proactively reported."""
|
||||
return (
|
||||
self._prefs.alexa_enabled
|
||||
@ -170,7 +175,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
||||
)
|
||||
|
||||
@property
|
||||
def endpoint(self):
|
||||
def endpoint(self) -> Any | None:
|
||||
"""Endpoint for report state."""
|
||||
if self._endpoint is None:
|
||||
raise ValueError("No endpoint available. Fetch access token first")
|
||||
@ -178,22 +183,22 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
||||
return self._endpoint
|
||||
|
||||
@property
|
||||
def locale(self):
|
||||
def locale(self) -> str:
|
||||
"""Return config locale."""
|
||||
# Not clear how to determine locale atm.
|
||||
return "en-US"
|
||||
|
||||
@property
|
||||
def entity_config(self):
|
||||
def entity_config(self) -> dict[str, Any]:
|
||||
"""Return entity config."""
|
||||
return self._config.get(CONF_ENTITY_CONFIG) or {}
|
||||
|
||||
@callback
|
||||
def user_identifier(self):
|
||||
def user_identifier(self) -> str:
|
||||
"""Return an identifier for the user that represents this config."""
|
||||
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."""
|
||||
if not self._config[CONF_FILTER].empty_filter:
|
||||
# Don't migrate if there's a YAML config
|
||||
@ -210,11 +215,11 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
||||
self._should_expose_legacy(entity_id),
|
||||
)
|
||||
|
||||
async def async_initialize(self):
|
||||
async def async_initialize(self) -> None:
|
||||
"""Initialize the Alexa config."""
|
||||
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 < 2 or (
|
||||
# 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
|
||||
)
|
||||
|
||||
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:
|
||||
await async_setup_component(self.hass, ALEXA_DOMAIN, {})
|
||||
|
||||
@ -248,14 +253,14 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
||||
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 entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
|
||||
return False
|
||||
|
||||
entity_configs = self._prefs.alexa_entity_configs
|
||||
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:
|
||||
return entity_expose
|
||||
|
||||
@ -279,21 +284,22 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
||||
)
|
||||
|
||||
@callback
|
||||
def should_expose(self, entity_id):
|
||||
def should_expose(self, entity_id: str) -> bool:
|
||||
"""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:
|
||||
return False
|
||||
return self._config[CONF_FILTER](entity_id)
|
||||
return entity_filter(entity_id)
|
||||
|
||||
return async_should_expose(self.hass, CLOUD_ALEXA, entity_id)
|
||||
|
||||
@callback
|
||||
def async_invalidate_access_token(self):
|
||||
def async_invalidate_access_token(self) -> None:
|
||||
"""Invalidate access token."""
|
||||
self._token_valid = None
|
||||
|
||||
async def async_get_access_token(self):
|
||||
async def async_get_access_token(self) -> Any:
|
||||
"""Get an access token."""
|
||||
if self._token_valid is not None and self._token_valid > utcnow():
|
||||
return self._token
|
||||
@ -380,7 +386,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
||||
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."""
|
||||
self._alexa_sync_unsub = None
|
||||
old_prefs = self._cur_entity_prefs
|
||||
@ -432,7 +438,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
||||
if await self._sync_helper(to_update, to_remove):
|
||||
self._cur_entity_prefs = new_prefs
|
||||
|
||||
async def async_sync_entities(self):
|
||||
async def async_sync_entities(self) -> bool:
|
||||
"""Sync all entities to Alexa."""
|
||||
# Remove any pending sync
|
||||
if self._alexa_sync_unsub:
|
||||
@ -452,7 +458,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
||||
|
||||
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.
|
||||
|
||||
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)
|
||||
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."""
|
||||
if not self.enabled or not self._cloud.is_logged_in:
|
||||
return
|
||||
|
@ -2,6 +2,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from hass_nabucasa import Cloud
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
@ -13,6 +17,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .client import CloudClient
|
||||
from .const import DISPATCHER_REMOTE_UPDATE, DOMAIN
|
||||
|
||||
WAIT_UNTIL_CHANGE = 3
|
||||
@ -41,10 +46,10 @@ class CloudRemoteBinary(BinarySensorEntity):
|
||||
_attr_unique_id = "cloud-remote-ui-connectivity"
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
|
||||
def __init__(self, cloud):
|
||||
def __init__(self, cloud: Cloud[CloudClient]) -> None:
|
||||
"""Initialize the binary sensor."""
|
||||
self.cloud = cloud
|
||||
self._unsub_dispatcher = None
|
||||
self._unsub_dispatcher: Callable[[], None] | None = None
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
@ -59,7 +64,7 @@ class CloudRemoteBinary(BinarySensorEntity):
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register update dispatcher."""
|
||||
|
||||
async def async_state_update(data):
|
||||
async def async_state_update(data: Any) -> None:
|
||||
"""Update callback."""
|
||||
await asyncio.sleep(WAIT_UNTIL_CHANGE)
|
||||
self.async_write_ha_state()
|
||||
|
@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
from pathlib import Path
|
||||
@ -95,7 +96,9 @@ class CloudClient(Interface):
|
||||
if self._alexa_config is None:
|
||||
async with self._alexa_config_init_lock:
|
||||
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()
|
||||
|
||||
@ -136,7 +139,7 @@ class CloudClient(Interface):
|
||||
"""When cloud is connected."""
|
||||
is_new_user = await self.prefs.async_set_username(self.cloud.username)
|
||||
|
||||
async def enable_alexa(_):
|
||||
async def enable_alexa(_: Any) -> None:
|
||||
"""Enable Alexa."""
|
||||
aconf = await self.get_alexa_config()
|
||||
try:
|
||||
@ -156,7 +159,7 @@ class CloudClient(Interface):
|
||||
|
||||
enable_alexa_job = HassJob(enable_alexa, cancel_on_shutdown=True)
|
||||
|
||||
async def enable_google(_):
|
||||
async def enable_google(_: datetime) -> None:
|
||||
"""Enable Google."""
|
||||
gconf = await self.get_google_config()
|
||||
|
||||
@ -210,7 +213,7 @@ class CloudClient(Interface):
|
||||
"""Process cloud alexa message to client."""
|
||||
cloud_user = await self._prefs.get_cloud_user()
|
||||
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,
|
||||
aconfig,
|
||||
payload,
|
||||
@ -223,9 +226,11 @@ class CloudClient(Interface):
|
||||
gconf = await self.get_google_config()
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
|
@ -1,8 +1,10 @@
|
||||
"""Google config for Cloud."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from hass_nabucasa import Cloud, cloud_api
|
||||
from hass_nabucasa.google_report_state import ErrorResponse
|
||||
@ -24,12 +26,14 @@ from homeassistant.core import (
|
||||
CoreState,
|
||||
Event,
|
||||
HomeAssistant,
|
||||
State,
|
||||
callback,
|
||||
split_entity_id,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er, start
|
||||
from homeassistant.helpers.entity import get_device_class
|
||||
from homeassistant.helpers.entityfilter import EntityFilter
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .const import (
|
||||
@ -42,6 +46,9 @@ from .const import (
|
||||
)
|
||||
from .prefs import GOOGLE_SETTINGS_VERSION, CloudPreferences
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .client import CloudClient
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CLOUD_GOOGLE = f"{CLOUD_DOMAIN}.{GOOGLE_DOMAIN}"
|
||||
@ -123,7 +130,7 @@ class CloudGoogleConfig(AbstractConfig):
|
||||
config: dict[str, Any],
|
||||
cloud_user: str,
|
||||
prefs: CloudPreferences,
|
||||
cloud: Cloud,
|
||||
cloud: Cloud[CloudClient],
|
||||
) -> None:
|
||||
"""Initialize the Google config."""
|
||||
super().__init__(hass)
|
||||
@ -134,7 +141,7 @@ class CloudGoogleConfig(AbstractConfig):
|
||||
self._sync_entities_lock = asyncio.Lock()
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
def enabled(self) -> bool:
|
||||
"""Return if Google is enabled."""
|
||||
return (
|
||||
self._cloud.is_logged_in
|
||||
@ -143,34 +150,34 @@ class CloudGoogleConfig(AbstractConfig):
|
||||
)
|
||||
|
||||
@property
|
||||
def entity_config(self):
|
||||
def entity_config(self) -> dict[str, Any]:
|
||||
"""Return entity config."""
|
||||
return self._config.get(CONF_ENTITY_CONFIG) or {}
|
||||
|
||||
@property
|
||||
def secure_devices_pin(self):
|
||||
def secure_devices_pin(self) -> str | None:
|
||||
"""Return entity config."""
|
||||
return self._prefs.google_secure_devices_pin
|
||||
|
||||
@property
|
||||
def should_report_state(self):
|
||||
def should_report_state(self) -> bool:
|
||||
"""Return if states should be proactively reported."""
|
||||
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 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 self._user
|
||||
|
||||
@property
|
||||
def cloud_user(self):
|
||||
def cloud_user(self) -> str:
|
||||
"""Return Cloud User account."""
|
||||
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."""
|
||||
if not self._config[CONF_FILTER].empty_filter:
|
||||
# Don't migrate if there's a YAML config
|
||||
@ -195,7 +202,7 @@ class CloudGoogleConfig(AbstractConfig):
|
||||
_2fa_disabled,
|
||||
)
|
||||
|
||||
async def async_initialize(self):
|
||||
async def async_initialize(self) -> None:
|
||||
"""Perform async initialization of config."""
|
||||
await super().async_initialize()
|
||||
|
||||
@ -246,18 +253,18 @@ class CloudGoogleConfig(AbstractConfig):
|
||||
self._handle_device_registry_updated,
|
||||
)
|
||||
|
||||
def should_expose(self, state):
|
||||
def should_expose(self, state: State) -> bool:
|
||||
"""If a state object should be exposed."""
|
||||
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 entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
|
||||
return False
|
||||
|
||||
entity_configs = self._prefs.google_entity_configs
|
||||
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:
|
||||
return entity_expose
|
||||
|
||||
@ -282,36 +289,37 @@ class CloudGoogleConfig(AbstractConfig):
|
||||
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 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:
|
||||
return False
|
||||
return self._config[CONF_FILTER](entity_id)
|
||||
return entity_filter(entity_id)
|
||||
|
||||
return async_should_expose(self.hass, CLOUD_GOOGLE, entity_id)
|
||||
|
||||
@property
|
||||
def agent_user_id(self):
|
||||
def agent_user_id(self) -> str:
|
||||
"""Return Agent User Id to use for query responses."""
|
||||
return self._cloud.username
|
||||
|
||||
@property
|
||||
def has_registered_user_agent(self):
|
||||
def has_registered_user_agent(self) -> bool:
|
||||
"""Return if we have a Agent User Id registered."""
|
||||
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."""
|
||||
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."""
|
||||
entity_configs = self._prefs.google_entity_configs
|
||||
entity_config = entity_configs.get(entity_id, {})
|
||||
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."""
|
||||
try:
|
||||
settings = async_get_entity_settings(self.hass, state.entity_id)
|
||||
@ -322,14 +330,14 @@ class CloudGoogleConfig(AbstractConfig):
|
||||
assistant_options = settings.get(CLOUD_GOOGLE, {})
|
||||
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."""
|
||||
try:
|
||||
await self._cloud.google_report_state.async_send_message(message)
|
||||
except ErrorResponse as err:
|
||||
_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."""
|
||||
if self._sync_entities_lock.locked():
|
||||
return HTTPStatus.OK
|
||||
@ -338,7 +346,7 @@ class CloudGoogleConfig(AbstractConfig):
|
||||
resp = await cloud_api.async_google_actions_request_sync(self._cloud)
|
||||
return resp.status
|
||||
|
||||
async def _async_prefs_updated(self, prefs):
|
||||
async def _async_prefs_updated(self, prefs: CloudPreferences) -> None:
|
||||
"""Handle updated preferences."""
|
||||
if not self._cloud.is_logged_in:
|
||||
if self.is_reporting_state:
|
||||
|
@ -1,14 +1,15 @@
|
||||
"""The HTTP api to control the cloud integration."""
|
||||
import asyncio
|
||||
from collections.abc import Mapping
|
||||
from collections.abc import Awaitable, Callable, Coroutine, Mapping
|
||||
from contextlib import suppress
|
||||
import dataclasses
|
||||
from functools import wraps
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, Concatenate, ParamSpec, TypeVar
|
||||
|
||||
import aiohttp
|
||||
from aiohttp import web
|
||||
import async_timeout
|
||||
import attr
|
||||
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 .alexa_config import entity_supported as entity_supported_by_alexa
|
||||
from .client import CloudClient
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
PREF_ALEXA_REPORT_STATE,
|
||||
@ -50,7 +52,7 @@ from .subscription import async_subscription_info
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
_CLOUD_ERRORS = {
|
||||
_CLOUD_ERRORS: dict[type[Exception], tuple[HTTPStatus, str]] = {
|
||||
asyncio.TimeoutError: (
|
||||
HTTPStatus.BAD_GATEWAY,
|
||||
"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."""
|
||||
websocket_api.async_register_command(hass, websocket_cloud_status)
|
||||
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."""
|
||||
|
||||
@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."""
|
||||
try:
|
||||
result = await handler(view, request, *args, **kwargs)
|
||||
@ -126,25 +138,37 @@ def _handle_cloud_errors(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."""
|
||||
|
||||
@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."""
|
||||
try:
|
||||
return await handler(hass, connection, msg)
|
||||
|
||||
except Exception as err: # pylint: disable=broad-except
|
||||
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
|
||||
|
||||
|
||||
def _process_cloud_exception(exc, where):
|
||||
def _process_cloud_exception(exc: Exception, where: str) -> tuple[HTTPStatus, str]:
|
||||
"""Process a cloud exception."""
|
||||
err_info = None
|
||||
err_info: tuple[HTTPStatus, str] | None = None
|
||||
|
||||
for err, value_info in _CLOUD_ERRORS.items():
|
||||
if isinstance(exc, err):
|
||||
@ -165,10 +189,10 @@ class GoogleActionsSyncView(HomeAssistantView):
|
||||
name = "api:cloud:google_actions/sync"
|
||||
|
||||
@_handle_cloud_errors
|
||||
async def post(self, request):
|
||||
async def post(self, request: web.Request) -> web.Response:
|
||||
"""Trigger a Google Actions sync."""
|
||||
hass = request.app["hass"]
|
||||
cloud: Cloud = hass.data[DOMAIN]
|
||||
cloud: Cloud[CloudClient] = hass.data[DOMAIN]
|
||||
gconf = await cloud.client.get_google_config()
|
||||
status = await gconf.async_sync_entities(gconf.agent_user_id)
|
||||
return self.json({}, status_code=status)
|
||||
@ -184,7 +208,7 @@ class CloudLoginView(HomeAssistantView):
|
||||
@RequestDataValidator(
|
||||
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."""
|
||||
|
||||
def cloud_assist_pipeline(hass: HomeAssistant) -> str | None:
|
||||
@ -221,7 +245,7 @@ class CloudLogoutView(HomeAssistantView):
|
||||
name = "api:cloud:logout"
|
||||
|
||||
@_handle_cloud_errors
|
||||
async def post(self, request):
|
||||
async def post(self, request: web.Request) -> web.Response:
|
||||
"""Handle logout request."""
|
||||
hass = request.app["hass"]
|
||||
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."""
|
||||
hass = request.app["hass"]
|
||||
cloud = hass.data[DOMAIN]
|
||||
@ -283,7 +307,7 @@ class CloudResendConfirmView(HomeAssistantView):
|
||||
|
||||
@_handle_cloud_errors
|
||||
@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."""
|
||||
hass = request.app["hass"]
|
||||
cloud = hass.data[DOMAIN]
|
||||
@ -302,7 +326,7 @@ class CloudForgotPasswordView(HomeAssistantView):
|
||||
|
||||
@_handle_cloud_errors
|
||||
@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."""
|
||||
hass = request.app["hass"]
|
||||
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."""
|
||||
|
||||
@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."""
|
||||
cloud = hass.data[DOMAIN]
|
||||
if not cloud.is_logged_in:
|
||||
@ -467,7 +500,9 @@ async def websocket_hook_delete(
|
||||
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."""
|
||||
|
||||
assert hass.config.api
|
||||
|
@ -1,14 +1,15 @@
|
||||
"""Preference management for cloud."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Coroutine
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.auth.const import GROUP_ID_ADMIN
|
||||
from homeassistant.auth.models import User
|
||||
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.typing import UNDEFINED
|
||||
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
|
||||
from homeassistant.util.logging import async_create_catching_coro
|
||||
|
||||
from .const import (
|
||||
@ -63,17 +64,20 @@ class CloudPreferencesStore(Store):
|
||||
class CloudPreferences:
|
||||
"""Handle cloud preferences."""
|
||||
|
||||
def __init__(self, hass):
|
||||
_prefs: dict[str, Any]
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize cloud prefs."""
|
||||
self._hass = hass
|
||||
self._store = CloudPreferencesStore(
|
||||
hass, STORAGE_VERSION, STORAGE_KEY, minor_version=STORAGE_VERSION_MINOR
|
||||
)
|
||||
self._prefs = None
|
||||
self._listeners = []
|
||||
self._listeners: list[
|
||||
Callable[[CloudPreferences], Coroutine[Any, Any, None]]
|
||||
] = []
|
||||
self.last_updated: set[str] = set()
|
||||
|
||||
async def async_initialize(self):
|
||||
async def async_initialize(self) -> None:
|
||||
"""Finish initializing the preferences."""
|
||||
if (prefs := await self._store.async_load()) is None:
|
||||
prefs = self._empty_config("")
|
||||
@ -89,26 +93,28 @@ class CloudPreferences:
|
||||
)
|
||||
|
||||
@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."""
|
||||
self._listeners.append(listener)
|
||||
|
||||
async def async_update(
|
||||
self,
|
||||
*,
|
||||
google_enabled=UNDEFINED,
|
||||
alexa_enabled=UNDEFINED,
|
||||
remote_enabled=UNDEFINED,
|
||||
google_secure_devices_pin=UNDEFINED,
|
||||
cloudhooks=UNDEFINED,
|
||||
cloud_user=UNDEFINED,
|
||||
alexa_report_state=UNDEFINED,
|
||||
google_report_state=UNDEFINED,
|
||||
tts_default_voice=UNDEFINED,
|
||||
remote_domain=UNDEFINED,
|
||||
alexa_settings_version=UNDEFINED,
|
||||
google_settings_version=UNDEFINED,
|
||||
):
|
||||
google_enabled: bool | UndefinedType = UNDEFINED,
|
||||
alexa_enabled: bool | UndefinedType = UNDEFINED,
|
||||
remote_enabled: bool | UndefinedType = UNDEFINED,
|
||||
google_secure_devices_pin: str | None | UndefinedType = UNDEFINED,
|
||||
cloudhooks: dict[str, dict[str, str | bool]] | UndefinedType = UNDEFINED,
|
||||
cloud_user: str | UndefinedType = UNDEFINED,
|
||||
alexa_report_state: bool | UndefinedType = UNDEFINED,
|
||||
google_report_state: bool | UndefinedType = UNDEFINED,
|
||||
tts_default_voice: tuple[str, str] | UndefinedType = UNDEFINED,
|
||||
remote_domain: str | None | UndefinedType = UNDEFINED,
|
||||
alexa_settings_version: int | UndefinedType = UNDEFINED,
|
||||
google_settings_version: int | UndefinedType = UNDEFINED,
|
||||
) -> None:
|
||||
"""Update user preferences."""
|
||||
prefs = {**self._prefs}
|
||||
|
||||
@ -131,7 +137,7 @@ class CloudPreferences:
|
||||
|
||||
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."""
|
||||
# Logging out.
|
||||
if username is None:
|
||||
@ -154,7 +160,7 @@ class CloudPreferences:
|
||||
|
||||
return True
|
||||
|
||||
def as_dict(self):
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
"""Return dictionary version."""
|
||||
return {
|
||||
PREF_ALEXA_DEFAULT_EXPOSE: self.alexa_default_expose,
|
||||
@ -170,7 +176,7 @@ class CloudPreferences:
|
||||
}
|
||||
|
||||
@property
|
||||
def remote_enabled(self):
|
||||
def remote_enabled(self) -> bool:
|
||||
"""Return if remote is enabled on start."""
|
||||
if not self._prefs.get(PREF_ENABLE_REMOTE, False):
|
||||
return False
|
||||
@ -178,17 +184,18 @@ class CloudPreferences:
|
||||
return True
|
||||
|
||||
@property
|
||||
def remote_domain(self):
|
||||
def remote_domain(self) -> str | None:
|
||||
"""Return remote domain."""
|
||||
return self._prefs.get(PREF_REMOTE_DOMAIN)
|
||||
|
||||
@property
|
||||
def alexa_enabled(self):
|
||||
def alexa_enabled(self) -> bool:
|
||||
"""Return if Alexa is enabled."""
|
||||
return self._prefs[PREF_ENABLE_ALEXA]
|
||||
alexa_enabled: bool = self._prefs[PREF_ENABLE_ALEXA]
|
||||
return alexa_enabled
|
||||
|
||||
@property
|
||||
def alexa_report_state(self):
|
||||
def alexa_report_state(self) -> bool:
|
||||
"""Return if Alexa report state is enabled."""
|
||||
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)
|
||||
|
||||
@property
|
||||
def alexa_entity_configs(self):
|
||||
def alexa_entity_configs(self) -> dict[str, Any]:
|
||||
"""Return Alexa Entity configurations."""
|
||||
return self._prefs.get(PREF_ALEXA_ENTITY_CONFIGS, {})
|
||||
|
||||
@property
|
||||
def alexa_settings_version(self):
|
||||
def alexa_settings_version(self) -> int:
|
||||
"""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
|
||||
def google_enabled(self):
|
||||
def google_enabled(self) -> bool:
|
||||
"""Return if Google is enabled."""
|
||||
return self._prefs[PREF_ENABLE_GOOGLE]
|
||||
google_enabled: bool = self._prefs[PREF_ENABLE_GOOGLE]
|
||||
return google_enabled
|
||||
|
||||
@property
|
||||
def google_report_state(self):
|
||||
def google_report_state(self) -> bool:
|
||||
"""Return if Google report state is enabled."""
|
||||
return self._prefs.get(PREF_GOOGLE_REPORT_STATE, DEFAULT_GOOGLE_REPORT_STATE)
|
||||
|
||||
@property
|
||||
def google_secure_devices_pin(self):
|
||||
def google_secure_devices_pin(self) -> str | None:
|
||||
"""Return if Google is allowed to unlock locks."""
|
||||
return self._prefs.get(PREF_GOOGLE_SECURE_DEVICES_PIN)
|
||||
|
||||
@property
|
||||
def google_entity_configs(self):
|
||||
def google_entity_configs(self) -> dict[str, dict[str, Any]]:
|
||||
"""Return Google Entity configurations."""
|
||||
return self._prefs.get(PREF_GOOGLE_ENTITY_CONFIGS, {})
|
||||
|
||||
@property
|
||||
def google_settings_version(self):
|
||||
def google_settings_version(self) -> int:
|
||||
"""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
|
||||
def google_local_webhook_id(self):
|
||||
def google_local_webhook_id(self) -> str:
|
||||
"""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
|
||||
def google_default_expose(self) -> list[str] | None:
|
||||
@ -249,12 +260,12 @@ class CloudPreferences:
|
||||
return self._prefs.get(PREF_GOOGLE_DEFAULT_EXPOSE)
|
||||
|
||||
@property
|
||||
def cloudhooks(self):
|
||||
def cloudhooks(self) -> dict[str, Any]:
|
||||
"""Return the published cloud webhooks."""
|
||||
return self._prefs.get(PREF_CLOUDHOOKS, {})
|
||||
|
||||
@property
|
||||
def tts_default_voice(self):
|
||||
def tts_default_voice(self) -> tuple[str, str]:
|
||||
"""Return the default TTS 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.
|
||||
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."""
|
||||
self.last_updated = {
|
||||
key for key, value in prefs.items() if value != self._prefs.get(key)
|
||||
@ -294,7 +305,7 @@ class CloudPreferences:
|
||||
|
||||
@callback
|
||||
@staticmethod
|
||||
def _empty_config(username):
|
||||
def _empty_config(username: str) -> dict[str, Any]:
|
||||
"""Return an empty config."""
|
||||
return {
|
||||
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.helpers import issue_registry as ir
|
||||
|
||||
from .client import CloudClient
|
||||
from .const import DOMAIN
|
||||
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:
|
||||
"""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:
|
||||
flow_manager = repairs_flow_manager(self.hass)
|
||||
|
@ -18,15 +18,22 @@ from homeassistant.components.stt import (
|
||||
SpeechResult,
|
||||
SpeechResultState,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .client import CloudClient
|
||||
from .const import DOMAIN
|
||||
|
||||
_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."""
|
||||
cloud: Cloud = hass.data[DOMAIN]
|
||||
cloud: Cloud[CloudClient] = hass.data[DOMAIN]
|
||||
|
||||
cloud_provider = CloudProvider(cloud)
|
||||
if discovery_info is not None:
|
||||
@ -37,7 +44,7 @@ async def async_get_engine(hass, config, discovery_info=None):
|
||||
class CloudProvider(Provider):
|
||||
"""NabuCasa speech API provider."""
|
||||
|
||||
def __init__(self, cloud: Cloud) -> None:
|
||||
def __init__(self, cloud: Cloud[CloudClient]) -> None:
|
||||
"""Home Assistant NabuCasa Speech to text."""
|
||||
self.cloud = cloud
|
||||
|
||||
|
@ -9,12 +9,13 @@ from aiohttp.client_exceptions import ClientError
|
||||
import async_timeout
|
||||
from hass_nabucasa import Cloud, cloud_api
|
||||
|
||||
from .client import CloudClient
|
||||
from .const import REQUEST_TIMEOUT
|
||||
|
||||
_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."""
|
||||
try:
|
||||
async with async_timeout.timeout(REQUEST_TIMEOUT):
|
||||
@ -33,7 +34,9 @@ async def async_subscription_info(cloud: Cloud) -> dict[str, Any] | 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."""
|
||||
try:
|
||||
async with async_timeout.timeout(REQUEST_TIMEOUT):
|
||||
|
@ -1,4 +1,6 @@
|
||||
"""Provide info to system health."""
|
||||
from typing import Any
|
||||
|
||||
from hass_nabucasa import Cloud
|
||||
|
||||
from homeassistant.components import system_health
|
||||
@ -16,12 +18,12 @@ def async_register(
|
||||
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."""
|
||||
cloud: Cloud = hass.data[DOMAIN]
|
||||
client: CloudClient = cloud.client
|
||||
cloud: Cloud[CloudClient] = hass.data[DOMAIN]
|
||||
client = cloud.client
|
||||
|
||||
data = {
|
||||
data: dict[str, Any] = {
|
||||
"logged_in": cloud.is_logged_in,
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
"""Support for the cloud for text to speech service."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from hass_nabucasa import Cloud
|
||||
from hass_nabucasa.voice import MAP_VOICE, TTS_VOICES, AudioOutput, VoiceError
|
||||
@ -12,11 +14,15 @@ from homeassistant.components.tts import (
|
||||
CONF_LANG,
|
||||
PLATFORM_SCHEMA,
|
||||
Provider,
|
||||
TtsAudioType,
|
||||
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 .prefs import CloudPreferences
|
||||
|
||||
ATTR_GENDER = "gender"
|
||||
|
||||
@ -25,7 +31,7 @@ SUPPORT_LANGUAGES = list(TTS_VOICES)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def validate_lang(value):
|
||||
def validate_lang(value: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate chosen gender or language."""
|
||||
if (lang := value.get(CONF_LANG)) is None:
|
||||
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."""
|
||||
cloud: Cloud = hass.data[DOMAIN]
|
||||
cloud: Cloud[CloudClient] = hass.data[DOMAIN]
|
||||
|
||||
language: str | None
|
||||
gender: str | None
|
||||
if discovery_info is not None:
|
||||
language = None
|
||||
gender = None
|
||||
@ -72,7 +84,9 @@ async def async_get_engine(hass, config, discovery_info=None):
|
||||
class CloudProvider(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."""
|
||||
self.cloud = cloud
|
||||
self.name = "Cloud"
|
||||
@ -85,22 +99,22 @@ class CloudProvider(Provider):
|
||||
self._language, self._gender = cloud.client.prefs.tts_default_voice
|
||||
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."""
|
||||
self._language, self._gender = prefs.tts_default_voice
|
||||
|
||||
@property
|
||||
def default_language(self):
|
||||
def default_language(self) -> str | None:
|
||||
"""Return the default language."""
|
||||
return self._language
|
||||
|
||||
@property
|
||||
def supported_languages(self):
|
||||
def supported_languages(self) -> list[str]:
|
||||
"""Return list of supported languages."""
|
||||
return SUPPORT_LANGUAGES
|
||||
|
||||
@property
|
||||
def supported_options(self):
|
||||
def supported_options(self) -> list[str]:
|
||||
"""Return list of supported options like voice, emotion."""
|
||||
return [ATTR_GENDER, ATTR_VOICE, ATTR_AUDIO_OUTPUT]
|
||||
|
||||
@ -112,17 +126,20 @@ class CloudProvider(Provider):
|
||||
return [Voice(voice, voice) for voice in voices]
|
||||
|
||||
@property
|
||||
def default_options(self):
|
||||
def default_options(self) -> dict[str, Any]:
|
||||
"""Return a dict include default options."""
|
||||
return {
|
||||
ATTR_GENDER: self._gender,
|
||||
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."""
|
||||
# Process TTS
|
||||
try:
|
||||
assert options is not None
|
||||
data = await self.cloud.voice.process_tts(
|
||||
text=message,
|
||||
language=language,
|
||||
|
@ -21,7 +21,7 @@ from homeassistant.const import (
|
||||
CONF_NAME,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
from homeassistant.core import Context, HomeAssistant, State, callback
|
||||
from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant, State, callback
|
||||
from homeassistant.helpers import (
|
||||
area_registry as ar,
|
||||
device_registry as dr,
|
||||
@ -86,19 +86,19 @@ def _get_registry_entries(
|
||||
class AbstractConfig(ABC):
|
||||
"""Hold the configuration for Google Assistant."""
|
||||
|
||||
_store: GoogleConfigStore
|
||||
_unsub_report_state: Callable[[], None] | None = None
|
||||
|
||||
def __init__(self, hass):
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize abstract config."""
|
||||
self.hass = hass
|
||||
self._store = None
|
||||
self._google_sync_unsub = {}
|
||||
self._google_sync_unsub: dict[str, CALLBACK_TYPE] = {}
|
||||
self._local_sdk_active = False
|
||||
self._local_last_active: datetime | None = None
|
||||
self._local_sdk_version_warn = False
|
||||
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."""
|
||||
self._store = GoogleConfigStore(self.hass)
|
||||
await self._store.async_initialize()
|
||||
@ -195,7 +195,7 @@ class AbstractConfig(ABC):
|
||||
await gather(*jobs)
|
||||
|
||||
@callback
|
||||
def async_enable_report_state(self):
|
||||
def async_enable_report_state(self) -> None:
|
||||
"""Enable proactive mode."""
|
||||
# Circular dep
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
@ -205,7 +205,7 @@ class AbstractConfig(ABC):
|
||||
self._unsub_report_state = async_enable_report_state(self.hass, self)
|
||||
|
||||
@callback
|
||||
def async_disable_report_state(self):
|
||||
def async_disable_report_state(self) -> None:
|
||||
"""Disable report state."""
|
||||
if self._unsub_report_state is not None:
|
||||
self._unsub_report_state()
|
||||
@ -220,7 +220,7 @@ class AbstractConfig(ABC):
|
||||
await self.async_disconnect_agent_user(agent_user_id)
|
||||
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."""
|
||||
if not self._store.agent_user_ids:
|
||||
return 204
|
||||
@ -249,7 +249,7 @@ class AbstractConfig(ABC):
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_schedule_google_sync_all(self):
|
||||
def async_schedule_google_sync_all(self) -> None:
|
||||
"""Schedule a sync for all registered agents."""
|
||||
for agent_user_id in self._store.agent_user_ids:
|
||||
self.async_schedule_google_sync(agent_user_id)
|
||||
@ -279,7 +279,7 @@ class AbstractConfig(ABC):
|
||||
self._store.pop_agent_user_id(agent_user_id)
|
||||
|
||||
@callback
|
||||
def async_enable_local_sdk(self):
|
||||
def async_enable_local_sdk(self) -> None:
|
||||
"""Enable the local SDK."""
|
||||
setup_successful = True
|
||||
setup_webhook_ids = []
|
||||
@ -323,7 +323,7 @@ class AbstractConfig(ABC):
|
||||
self._local_sdk_active = setup_successful
|
||||
|
||||
@callback
|
||||
def async_disable_local_sdk(self):
|
||||
def async_disable_local_sdk(self) -> None:
|
||||
"""Disable the local SDK."""
|
||||
if not self._local_sdk_active:
|
||||
return
|
||||
@ -500,7 +500,7 @@ class GoogleEntity:
|
||||
self.hass = hass
|
||||
self.config = config
|
||||
self.state = state
|
||||
self._traits = None
|
||||
self._traits: list[trait._Trait] | None = None
|
||||
|
||||
@property
|
||||
def entity_id(self):
|
||||
@ -508,7 +508,7 @@ class GoogleEntity:
|
||||
return self.state.entity_id
|
||||
|
||||
@callback
|
||||
def traits(self):
|
||||
def traits(self) -> list[trait._Trait]:
|
||||
"""Return traits for entity."""
|
||||
if self._traits is not None:
|
||||
return self._traits
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""Implement the Google Smart Home traits."""
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
import logging
|
||||
from typing import Any, TypeVar
|
||||
|
||||
@ -196,9 +197,10 @@ def _next_selected(items: list[str], selected: str | None) -> str | None:
|
||||
return items[next_item]
|
||||
|
||||
|
||||
class _Trait:
|
||||
class _Trait(ABC):
|
||||
"""Represents a Trait inside Google Assistant skill."""
|
||||
|
||||
name: str
|
||||
commands: list[str] = []
|
||||
|
||||
@staticmethod
|
||||
@ -206,6 +208,11 @@ class _Trait:
|
||||
"""Return if the trait might ask for 2FA."""
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
@abstractmethod
|
||||
def supported(domain, features, device_class, attributes):
|
||||
"""Test if state is supported."""
|
||||
|
||||
def __init__(self, hass, state, config):
|
||||
"""Initialize a trait for a state."""
|
||||
self.hass = hass
|
||||
|
10
mypy.ini
10
mypy.ini
@ -631,6 +631,16 @@ disallow_untyped_defs = true
|
||||
warn_return_any = 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.*]
|
||||
check_untyped_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:
|
||||
"""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
|
||||
|
||||
|
Reference in New Issue
Block a user