diff --git a/.strict-typing b/.strict-typing index 9ed4bf53497..958c53d07db 100644 --- a/.strict-typing +++ b/.strict-typing @@ -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.* diff --git a/homeassistant/components/alexa/config.py b/homeassistant/components/alexa/config.py index e086d525cf1..d47a548979e 100644 --- a/homeassistant/components/alexa/config.py +++ b/homeassistant/components/alexa/config.py @@ -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. diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index a189c364c02..ebab3bcee8c 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -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 diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 0af85fe9d4d..620e650315a 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -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() diff --git a/homeassistant/components/cloud/account_link.py b/homeassistant/components/cloud/account_link.py index e3b3c1231bb..1423330cb44 100644 --- a/homeassistant/components/cloud/account_link.py +++ b/homeassistant/components/cloud/account_link.py @@ -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.""" diff --git a/homeassistant/components/cloud/alexa_config.py b/homeassistant/components/cloud/alexa_config.py index 53bf44d8aa1..052fddabb54 100644 --- a/homeassistant/components/cloud/alexa_config.py +++ b/homeassistant/components/cloud/alexa_config.py @@ -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 diff --git a/homeassistant/components/cloud/binary_sensor.py b/homeassistant/components/cloud/binary_sensor.py index 2d78ad8b512..e09122ac7bf 100644 --- a/homeassistant/components/cloud/binary_sensor.py +++ b/homeassistant/components/cloud/binary_sensor.py @@ -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() diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index f3878fd68af..dff3bdcdbdd 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -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 ) diff --git a/homeassistant/components/cloud/google_config.py b/homeassistant/components/cloud/google_config.py index 351de5d0e65..8592a4ffbe3 100644 --- a/homeassistant/components/cloud/google_config.py +++ b/homeassistant/components/cloud/google_config.py @@ -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: diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index f5d5c98fe1a..84c348236d4 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -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 diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index 5ccc007e524..46ddafd48e7 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -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, diff --git a/homeassistant/components/cloud/repairs.py b/homeassistant/components/cloud/repairs.py index 0864d8b48ad..f7368731d92 100644 --- a/homeassistant/components/cloud/repairs.py +++ b/homeassistant/components/cloud/repairs.py @@ -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) diff --git a/homeassistant/components/cloud/stt.py b/homeassistant/components/cloud/stt.py index 84e1e088d47..7b6da8b7403 100644 --- a/homeassistant/components/cloud/stt.py +++ b/homeassistant/components/cloud/stt.py @@ -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 diff --git a/homeassistant/components/cloud/subscription.py b/homeassistant/components/cloud/subscription.py index b85a50b20cd..633f0c95e1b 100644 --- a/homeassistant/components/cloud/subscription.py +++ b/homeassistant/components/cloud/subscription.py @@ -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): diff --git a/homeassistant/components/cloud/system_health.py b/homeassistant/components/cloud/system_health.py index 592338144f3..0dfd69344f3 100644 --- a/homeassistant/components/cloud/system_health.py +++ b/homeassistant/components/cloud/system_health.py @@ -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, } diff --git a/homeassistant/components/cloud/tts.py b/homeassistant/components/cloud/tts.py index fea2ffca987..014b5c75d4c 100644 --- a/homeassistant/components/cloud/tts.py +++ b/homeassistant/components/cloud/tts.py @@ -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, diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index e194242df91..49d130d6656 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -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 diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 3752574f31f..a075a251dba 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -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 diff --git a/mypy.ini b/mypy.ini index b2c6b7a9c83..feb6f5a72ea 100644 --- a/mypy.ini +++ b/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 diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index bd0a4972241..f56789729d8 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -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