From 6376b4be5c51fd8791568372986c1a85b8526ceb Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 26 Jul 2021 16:43:52 -0700 Subject: [PATCH] Increase static type coverage for nest integration (#53475) Co-authored-by: Mick Vleeshouwer Co-authored-by: Franck Nijhof --- homeassistant/components/nest/__init__.py | 6 +- homeassistant/components/nest/api.py | 4 +- .../components/nest/binary_sensor.py | 3 +- homeassistant/components/nest/camera.py | 3 +- homeassistant/components/nest/camera_sdm.py | 56 ++++++++++------- homeassistant/components/nest/climate.py | 3 +- homeassistant/components/nest/climate_sdm.py | 62 ++++++++++--------- homeassistant/components/nest/config_flow.py | 10 ++- homeassistant/components/nest/device_info.py | 34 +++++----- .../components/nest/device_trigger.py | 12 ++-- .../components/nest/legacy/__init__.py | 4 +- .../components/nest/legacy/binary_sensor.py | 2 +- .../components/nest/legacy/camera.py | 2 +- .../components/nest/legacy/climate.py | 2 +- .../components/nest/legacy/sensor.py | 2 +- homeassistant/components/nest/sensor.py | 3 +- homeassistant/components/nest/sensor_sdm.py | 32 +++++----- tests/components/nest/device_info_test.py | 14 ++--- tests/components/nest/sensor_sdm_test.py | 2 +- 19 files changed, 146 insertions(+), 110 deletions(-) diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 1548814804b..b999b2e94e0 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -69,7 +69,7 @@ CONFIG_SCHEMA = vol.Schema( PLATFORMS = ["sensor", "camera", "climate"] -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: dict) -> bool: """Set up Nest components with dispatch between old/new flows.""" hass.data[DOMAIN] = {} @@ -109,7 +109,7 @@ class SignalUpdateCallback: """Initialize EventCallback.""" self._hass = hass - async def async_handle_event(self, event_message: EventMessage): + async def async_handle_event(self, event_message: EventMessage) -> None: """Process an incoming EventMessage.""" if not event_message.resource_update_name: return @@ -194,7 +194,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" if DATA_SDM not in entry.data: # Legacy API diff --git a/homeassistant/components/nest/api.py b/homeassistant/components/nest/api.py index 29f39f5aec3..8affad958b7 100644 --- a/homeassistant/components/nest/api.py +++ b/homeassistant/components/nest/api.py @@ -29,13 +29,13 @@ class AsyncConfigEntryAuth(AbstractAuth): self._client_id = client_id self._client_secret = client_secret - async def async_get_access_token(self): + async def async_get_access_token(self) -> str: """Return a valid access token for SDM API.""" if not self._oauth_session.valid_token: await self._oauth_session.async_ensure_token_valid() return self._oauth_session.token["access_token"] - async def async_get_creds(self): + async def async_get_creds(self) -> Credentials: """Return an OAuth credential for Pub/Sub Subscriber.""" # We don't have a way for Home Assistant to refresh creds on behalf # of the google pub/sub subscriber. Instead, build a full diff --git a/homeassistant/components/nest/binary_sensor.py b/homeassistant/components/nest/binary_sensor.py index 0bf65f2163c..6d9331744ef 100644 --- a/homeassistant/components/nest/binary_sensor.py +++ b/homeassistant/components/nest/binary_sensor.py @@ -2,13 +2,14 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_SDM from .legacy.binary_sensor import async_setup_legacy_entry async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the binary sensors.""" assert DATA_SDM not in entry.data diff --git a/homeassistant/components/nest/camera.py b/homeassistant/components/nest/camera.py index ca117f0cbf1..7ae3e0db943 100644 --- a/homeassistant/components/nest/camera.py +++ b/homeassistant/components/nest/camera.py @@ -2,6 +2,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .camera_sdm import async_setup_sdm_entry from .const import DATA_SDM @@ -9,7 +10,7 @@ from .legacy.camera import async_setup_legacy_entry async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the cameras.""" if DATA_SDM not in entry.data: diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index f8f2db506e2..862b6dbdffb 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -3,11 +3,14 @@ from __future__ import annotations import datetime import logging +from typing import Any, Callable from google_nest_sdm.camera_traits import ( CameraEventImageTrait, CameraImageTrait, CameraLiveStreamTrait, + CameraMotionTrait, + RtspStream, ) from google_nest_sdm.device import Device from google_nest_sdm.exceptions import GoogleNestException @@ -18,11 +21,13 @@ from homeassistant.components.ffmpeg import async_get_image from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.util.dt import utcnow from .const import DATA_SUBSCRIBER, DOMAIN -from .device_info import DeviceInfo +from .device_info import NestDeviceInfo _LOGGER = logging.getLogger(__name__) @@ -31,7 +36,7 @@ STREAM_EXPIRATION_BUFFER = datetime.timedelta(seconds=30) async def async_setup_sdm_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the cameras.""" @@ -60,13 +65,13 @@ class NestCamera(Camera): """Initialize the camera.""" super().__init__() self._device = device - self._device_info = DeviceInfo(device) - self._stream = None - self._stream_refresh_unsub = None + self._device_info = NestDeviceInfo(device) + self._stream: RtspStream | None = None + self._stream_refresh_unsub: Callable[[], None] | None = None # Cache of most recent event image - self._event_id = None - self._event_image_bytes = None - self._event_image_cleanup_unsub = None + self._event_id: str | None = None + self._event_image_bytes: bytes | None = None + self._event_image_cleanup_unsub: Callable[[], None] | None = None @property def should_poll(self) -> bool: @@ -74,40 +79,40 @@ class NestCamera(Camera): return False @property - def unique_id(self) -> str | None: + def unique_id(self) -> str: """Return a unique ID.""" # The API "name" field is a unique device identifier. return f"{self._device.name}-camera" @property - def name(self): + def name(self) -> str | None: """Return the name of the camera.""" return self._device_info.device_name @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device specific attributes.""" return self._device_info.device_info @property - def brand(self): + def brand(self) -> str | None: """Return the camera brand.""" return self._device_info.device_brand @property - def model(self): + def model(self) -> str | None: """Return the camera model.""" return self._device_info.device_model @property - def supported_features(self): + def supported_features(self) -> int: """Flag supported features.""" supported_features = 0 if CameraLiveStreamTrait.NAME in self._device.traits: supported_features |= SUPPORT_STREAM return supported_features - async def stream_source(self): + async def stream_source(self) -> str | None: """Return the source of the stream.""" if CameraLiveStreamTrait.NAME not in self._device.traits: return None @@ -120,8 +125,9 @@ class NestCamera(Camera): _LOGGER.warning("Stream already expired") return self._stream.rtsp_stream_url - def _schedule_stream_refresh(self): + def _schedule_stream_refresh(self) -> None: """Schedules an alarm to refresh the stream url before expiration.""" + assert self._stream _LOGGER.debug("New stream url expires at %s", self._stream.expires_at) refresh_time = self._stream.expires_at - STREAM_EXPIRATION_BUFFER # Schedule an alarm to extend the stream @@ -134,7 +140,7 @@ class NestCamera(Camera): refresh_time, ) - async def _handle_stream_refresh(self, now): + async def _handle_stream_refresh(self, now: datetime.datetime) -> None: """Alarm that fires to check if the stream should be refreshed.""" if not self._stream: return @@ -154,7 +160,7 @@ class NestCamera(Camera): self.stream.update_source(self._stream.rtsp_stream_url) self._schedule_stream_refresh() - async def async_will_remove_from_hass(self): + async def async_will_remove_from_hass(self) -> None: """Invalidates the RTSP token when unloaded.""" if self._stream: _LOGGER.debug("Invalidating stream") @@ -166,13 +172,13 @@ class NestCamera(Camera): if self._event_image_cleanup_unsub is not None: self._event_image_cleanup_unsub() - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when entity is added to register update signal handler.""" self.async_on_remove( self._device.add_update_listener(self.async_write_ha_state) ) - async def async_camera_image(self): + async def async_camera_image(self) -> bytes | None: """Return bytes of camera image.""" # Returns the snapshot of the last event for ~30 seconds after the event active_event_image = await self._async_active_event_image() @@ -184,7 +190,7 @@ class NestCamera(Camera): return None return await async_get_image(self.hass, stream_url, output_format=IMAGE_JPEG) - async def _async_active_event_image(self): + async def _async_active_event_image(self) -> bytes | None: """Return image from any active events happening.""" if CameraEventImageTrait.NAME not in self._device.traits: return None @@ -204,7 +210,9 @@ class NestCamera(Camera): self._schedule_event_image_cleanup(event.expires_at) return image_bytes - async def _async_fetch_active_event_image(self, trait): + async def _async_fetch_active_event_image( + self, trait: CameraMotionTrait + ) -> bytes | None: """Return image bytes for an active event.""" try: event_image = await trait.generate_active_event_image() @@ -219,7 +227,7 @@ class NestCamera(Camera): _LOGGER.debug("Unable to fetch event image: %s", err) return None - def _schedule_event_image_cleanup(self, point_in_time): + def _schedule_event_image_cleanup(self, point_in_time: datetime.datetime) -> None: """Schedules an alarm to remove the image bytes from memory, honoring expiration.""" if self._event_image_cleanup_unsub is not None: self._event_image_cleanup_unsub() @@ -229,7 +237,7 @@ class NestCamera(Camera): point_in_time, ) - def _handle_event_image_cleanup(self, now): + def _handle_event_image_cleanup(self, now: Any) -> None: """Clear images cached from events and scheduled callback.""" self._event_id = None self._event_image_bytes = None diff --git a/homeassistant/components/nest/climate.py b/homeassistant/components/nest/climate.py index 1644cc46004..372909d00c2 100644 --- a/homeassistant/components/nest/climate.py +++ b/homeassistant/components/nest/climate.py @@ -2,6 +2,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .climate_sdm import async_setup_sdm_entry from .const import DATA_SDM @@ -9,7 +10,7 @@ from .legacy.climate import async_setup_legacy_entry async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the climate platform.""" if DATA_SDM not in entry.data: diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index ab987ff332f..51daa7fbb9c 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -1,6 +1,8 @@ """Support for Google Nest SDM climate devices.""" from __future__ import annotations +from typing import Any + from google_nest_sdm.device import Device from google_nest_sdm.device_traits import FanTrait, TemperatureTrait from google_nest_sdm.exceptions import GoogleNestException @@ -37,12 +39,14 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_SUBSCRIBER, DOMAIN -from .device_info import DeviceInfo +from .device_info import NestDeviceInfo # Mapping for sdm.devices.traits.ThermostatMode mode field -THERMOSTAT_MODE_MAP = { +THERMOSTAT_MODE_MAP: dict[str, str] = { "OFF": HVAC_MODE_OFF, "HEAT": HVAC_MODE_HEAT, "COOL": HVAC_MODE_COOL, @@ -78,7 +82,7 @@ MAX_FAN_DURATION = 43200 # 15 hours is the max in the SDM API async def async_setup_sdm_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the client entities.""" @@ -101,7 +105,7 @@ class ThermostatEntity(ClimateEntity): def __init__(self, device: Device) -> None: """Initialize ThermostatEntity.""" self._device = device - self._device_info = DeviceInfo(device) + self._device_info = NestDeviceInfo(device) self._supported_features = 0 @property @@ -116,16 +120,16 @@ class ThermostatEntity(ClimateEntity): return self._device.name @property - def name(self): + def name(self) -> str | None: """Return the name of the entity.""" return self._device_info.device_name @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device specific attributes.""" return self._device_info.device_info - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when entity is added to register update signal handler.""" self._supported_features = self._get_supported_features() self.async_on_remove( @@ -133,20 +137,20 @@ class ThermostatEntity(ClimateEntity): ) @property - def temperature_unit(self): + def temperature_unit(self) -> str: """Return the unit of temperature measurement for the system.""" return TEMP_CELSIUS @property - def current_temperature(self): + def current_temperature(self) -> float | None: """Return the current temperature.""" if TemperatureTrait.NAME not in self._device.traits: return None - trait = self._device.traits[TemperatureTrait.NAME] + trait: TemperatureTrait = self._device.traits[TemperatureTrait.NAME] return trait.ambient_temperature_celsius @property - def target_temperature(self): + def target_temperature(self) -> float | None: """Return the temperature currently set to be reached.""" trait = self._target_temperature_trait if not trait: @@ -158,7 +162,7 @@ class ThermostatEntity(ClimateEntity): return None @property - def target_temperature_high(self): + def target_temperature_high(self) -> float | None: """Return the upper bound target temperature.""" if self.hvac_mode != HVAC_MODE_HEAT_COOL: return None @@ -168,7 +172,7 @@ class ThermostatEntity(ClimateEntity): return trait.cool_celsius @property - def target_temperature_low(self): + def target_temperature_low(self) -> float | None: """Return the lower bound target temperature.""" if self.hvac_mode != HVAC_MODE_HEAT_COOL: return None @@ -178,7 +182,9 @@ class ThermostatEntity(ClimateEntity): return trait.heat_celsius @property - def _target_temperature_trait(self): + def _target_temperature_trait( + self, + ) -> ThermostatEcoTrait | ThermostatTemperatureSetpointTrait | None: """Return the correct trait with a target temp depending on mode.""" if ( self.preset_mode == PRESET_ECO @@ -190,7 +196,7 @@ class ThermostatEntity(ClimateEntity): return None @property - def hvac_mode(self): + def hvac_mode(self) -> str: """Return the current operation (e.g. heat, cool, idle).""" hvac_mode = HVAC_MODE_OFF if ThermostatModeTrait.NAME in self._device.traits: @@ -202,7 +208,7 @@ class ThermostatEntity(ClimateEntity): return hvac_mode @property - def hvac_modes(self): + def hvac_modes(self) -> list[str]: """List of available operation modes.""" supported_modes = [] for mode in self._get_device_hvac_modes: @@ -213,7 +219,7 @@ class ThermostatEntity(ClimateEntity): return supported_modes @property - def _get_device_hvac_modes(self): + def _get_device_hvac_modes(self) -> set[str]: """Return the set of SDM API hvac modes supported by the device.""" modes = [] if ThermostatModeTrait.NAME in self._device.traits: @@ -222,7 +228,7 @@ class ThermostatEntity(ClimateEntity): return set(modes) @property - def hvac_action(self): + def hvac_action(self) -> str | None: """Return the current HVAC action (heating, cooling).""" trait = self._device.traits[ThermostatHvacTrait.NAME] if trait.status in THERMOSTAT_HVAC_STATUS_MAP: @@ -230,7 +236,7 @@ class ThermostatEntity(ClimateEntity): return None @property - def preset_mode(self): + def preset_mode(self) -> str: """Return the current active preset.""" if ThermostatEcoTrait.NAME in self._device.traits: trait = self._device.traits[ThermostatEcoTrait.NAME] @@ -238,7 +244,7 @@ class ThermostatEntity(ClimateEntity): return PRESET_NONE @property - def preset_modes(self): + def preset_modes(self) -> list[str]: """Return the available presets.""" modes = [] if ThermostatEcoTrait.NAME in self._device.traits: @@ -249,7 +255,7 @@ class ThermostatEntity(ClimateEntity): return modes @property - def fan_mode(self): + def fan_mode(self) -> str: """Return the current fan mode.""" if FanTrait.NAME in self._device.traits: trait = self._device.traits[FanTrait.NAME] @@ -257,7 +263,7 @@ class ThermostatEntity(ClimateEntity): return FAN_OFF @property - def fan_modes(self): + def fan_modes(self) -> list[str]: """Return the list of available fan modes.""" modes = [] if FanTrait.NAME in self._device.traits: @@ -265,11 +271,11 @@ class ThermostatEntity(ClimateEntity): return modes @property - def supported_features(self): + def supported_features(self) -> int: """Bitmap of supported features.""" return self._supported_features - def _get_supported_features(self): + def _get_supported_features(self) -> int: """Compute the bitmap of supported features from the current state.""" features = 0 if HVAC_MODE_HEAT_COOL in self.hvac_modes: @@ -285,7 +291,7 @@ class ThermostatEntity(ClimateEntity): features |= SUPPORT_FAN_MODE return features - async def async_set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" if hvac_mode not in self.hvac_modes: raise ValueError(f"Unsupported hvac_mode '{hvac_mode}'") @@ -297,7 +303,7 @@ class ThermostatEntity(ClimateEntity): trait = self._device.traits[ThermostatModeTrait.NAME] await trait.set_mode(api_mode) - async def async_set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW) high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) @@ -313,14 +319,14 @@ class ThermostatEntity(ClimateEntity): elif self.hvac_mode == HVAC_MODE_HEAT and temp: await trait.set_heat(temp) - async def async_set_preset_mode(self, preset_mode): + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new target preset mode.""" if preset_mode not in self.preset_modes: raise ValueError(f"Unsupported preset_mode '{preset_mode}'") trait = self._device.traits[ThermostatEcoTrait.NAME] await trait.set_mode(PRESET_INV_MODE_MAP[preset_mode]) - async def async_set_fan_mode(self, fan_mode): + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" if fan_mode not in self.fan_modes: raise ValueError(f"Unsupported fan_mode '{fan_mode}'") diff --git a/homeassistant/components/nest/config_flow.py b/homeassistant/components/nest/config_flow.py index eeeae4b1ddd..1ec3e421a0d 100644 --- a/homeassistant/components/nest/config_flow.py +++ b/homeassistant/components/nest/config_flow.py @@ -35,7 +35,13 @@ _LOGGER = logging.getLogger(__name__) @callback -def register_flow_implementation(hass, domain, name, gen_authorize_url, convert_code): +def register_flow_implementation( + hass: HomeAssistant, + domain: str, + name: str, + gen_authorize_url: str, + convert_code: str, +) -> None: """Register a flow implementation for legacy api. domain: Domain of the component responsible for the implementation. @@ -74,7 +80,7 @@ class NestFlowHandler( DOMAIN = DOMAIN VERSION = 1 - def __init__(self): + def __init__(self) -> None: """Initialize NestFlowHandler.""" super().__init__() # When invoked for reauth, allows updating an existing config entry diff --git a/homeassistant/components/nest/device_info.py b/homeassistant/components/nest/device_info.py index 579733de8ad..7b10fabcd61 100644 --- a/homeassistant/components/nest/device_info.py +++ b/homeassistant/components/nest/device_info.py @@ -1,11 +1,15 @@ """Library for extracting device specific information common to entities.""" +from __future__ import annotations + from google_nest_sdm.device import Device from google_nest_sdm.device_traits import InfoTrait +from homeassistant.helpers.entity import DeviceInfo + from .const import DOMAIN -DEVICE_TYPE_MAP = { +DEVICE_TYPE_MAP: dict[str, str] = { "sdm.devices.types.CAMERA": "Camera", "sdm.devices.types.DISPLAY": "Display", "sdm.devices.types.DOORBELL": "Doorbell", @@ -13,7 +17,7 @@ DEVICE_TYPE_MAP = { } -class DeviceInfo: +class NestDeviceInfo: """Provide device info from the SDM device, shared across platforms.""" device_brand = "Google Nest" @@ -23,21 +27,23 @@ class DeviceInfo: self._device = device @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device specific attributes.""" - return { - # The API "name" field is a unique device identifier. - "identifiers": {(DOMAIN, self._device.name)}, - "name": self.device_name, - "manufacturer": self.device_brand, - "model": self.device_model, - } + return DeviceInfo( + { + # The API "name" field is a unique device identifier. + "identifiers": {(DOMAIN, self._device.name)}, + "name": self.device_name, + "manufacturer": self.device_brand, + "model": self.device_model, + } + ) @property - def device_name(self): + def device_name(self) -> str: """Return the name of the physical device that includes the sensor.""" if InfoTrait.NAME in self._device.traits: - trait = self._device.traits[InfoTrait.NAME] + trait: InfoTrait = self._device.traits[InfoTrait.NAME] if trait.custom_name: return trait.custom_name # Build a name from the room/structure. Note: This room/structure name @@ -50,9 +56,9 @@ class DeviceInfo: return self.device_model @property - def device_model(self): + def device_model(self) -> str: """Return device model information.""" # The API intentionally returns minimal information about specific # devices, instead relying on traits, but we can infer a generic model # name based on the type - return DEVICE_TYPE_MAP.get(self._device.type) + return DEVICE_TYPE_MAP.get(self._device.type, "Unknown") diff --git a/homeassistant/components/nest/device_trigger.py b/homeassistant/components/nest/device_trigger.py index 889111d6f61..980d9726467 100644 --- a/homeassistant/components/nest/device_trigger.py +++ b/homeassistant/components/nest/device_trigger.py @@ -11,6 +11,7 @@ from homeassistant.components.device_automation.exceptions import ( from homeassistant.components.homeassistant.triggers import event as event_trigger from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.device_registry import DeviceRegistry from homeassistant.helpers.typing import ConfigType from .const import DATA_SUBSCRIBER, DOMAIN @@ -29,11 +30,14 @@ TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend( async def async_get_nest_device_id(hass: HomeAssistant, device_id: str) -> str | None: """Get the nest API device_id from the HomeAssistant device_id.""" - device_registry = await hass.helpers.device_registry.async_get_registry() + device_registry: DeviceRegistry = ( + await hass.helpers.device_registry.async_get_registry() + ) device = device_registry.async_get(device_id) - for (domain, unique_id) in device.identifiers: - if domain == DOMAIN: - return unique_id + if device: + for (domain, unique_id) in device.identifiers: + if domain == DOMAIN: + return unique_id return None diff --git a/homeassistant/components/nest/legacy/__init__.py b/homeassistant/components/nest/legacy/__init__.py index b0083dcf990..04f7b1ac663 100644 --- a/homeassistant/components/nest/legacy/__init__.py +++ b/homeassistant/components/nest/legacy/__init__.py @@ -96,7 +96,7 @@ def nest_update_event_broker(hass, nest): _LOGGER.debug("Stop listening for nest.update_event") -async def async_setup_legacy(hass, config): +async def async_setup_legacy(hass, config) -> bool: """Set up Nest components using the legacy nest API.""" if DOMAIN not in config: return True @@ -122,7 +122,7 @@ async def async_setup_legacy(hass, config): return True -async def async_setup_legacy_entry(hass, entry): +async def async_setup_legacy_entry(hass, entry) -> bool: """Set up Nest from legacy config entry.""" nest = Nest(access_token=entry.data["tokens"]["access_token"]) diff --git a/homeassistant/components/nest/legacy/binary_sensor.py b/homeassistant/components/nest/legacy/binary_sensor.py index 32c30f747d2..c257ddd9456 100644 --- a/homeassistant/components/nest/legacy/binary_sensor.py +++ b/homeassistant/components/nest/legacy/binary_sensor.py @@ -61,7 +61,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """ -async def async_setup_legacy_entry(hass, entry, async_add_entities): +async def async_setup_legacy_entry(hass, entry, async_add_entities) -> None: """Set up a Nest binary sensor based on a config entry.""" nest = hass.data[DATA_NEST] diff --git a/homeassistant/components/nest/legacy/camera.py b/homeassistant/components/nest/legacy/camera.py index cc9be9d7588..77629e4dcff 100644 --- a/homeassistant/components/nest/legacy/camera.py +++ b/homeassistant/components/nest/legacy/camera.py @@ -23,7 +23,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """ -async def async_setup_legacy_entry(hass, entry, async_add_entities): +async def async_setup_legacy_entry(hass, entry, async_add_entities) -> None: """Set up a Nest sensor based on a config entry.""" camera_devices = await hass.async_add_executor_job(hass.data[DATA_NEST].cameras) cameras = [NestCamera(structure, device) for structure, device in camera_devices] diff --git a/homeassistant/components/nest/legacy/climate.py b/homeassistant/components/nest/legacy/climate.py index cd0d66acba8..17448d9be8c 100644 --- a/homeassistant/components/nest/legacy/climate.py +++ b/homeassistant/components/nest/legacy/climate.py @@ -74,7 +74,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """ -async def async_setup_legacy_entry(hass, entry, async_add_entities): +async def async_setup_legacy_entry(hass, entry, async_add_entities) -> None: """Set up the Nest climate device based on a config entry.""" temp_unit = hass.config.units.temperature_unit diff --git a/homeassistant/components/nest/legacy/sensor.py b/homeassistant/components/nest/legacy/sensor.py index 54df3921bbd..0939e925b43 100644 --- a/homeassistant/components/nest/legacy/sensor.py +++ b/homeassistant/components/nest/legacy/sensor.py @@ -77,7 +77,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """ -async def async_setup_legacy_entry(hass, entry, async_add_entities): +async def async_setup_legacy_entry(hass, entry, async_add_entities) -> None: """Set up a Nest sensor based on a config entry.""" nest = hass.data[DATA_NEST] diff --git a/homeassistant/components/nest/sensor.py b/homeassistant/components/nest/sensor.py index c58ad26112d..a9073aec80d 100644 --- a/homeassistant/components/nest/sensor.py +++ b/homeassistant/components/nest/sensor.py @@ -2,6 +2,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_SDM from .legacy.sensor import async_setup_legacy_entry @@ -9,7 +10,7 @@ from .sensor_sdm import async_setup_sdm_entry async def async_setup_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the sensors.""" if DATA_SDM not in entry.data: diff --git a/homeassistant/components/nest/sensor_sdm.py b/homeassistant/components/nest/sensor_sdm.py index d1ff26880de..42614af8c40 100644 --- a/homeassistant/components/nest/sensor_sdm.py +++ b/homeassistant/components/nest/sensor_sdm.py @@ -17,9 +17,11 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_SUBSCRIBER, DOMAIN -from .device_info import DeviceInfo +from .device_info import NestDeviceInfo _LOGGER = logging.getLogger(__name__) @@ -33,7 +35,7 @@ DEVICE_TYPE_MAP = { async def async_setup_sdm_entry( - hass: HomeAssistant, entry: ConfigEntry, async_add_entities + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the sensors.""" @@ -59,7 +61,7 @@ class SensorBase(SensorEntity): def __init__(self, device: Device) -> None: """Initialize the sensor.""" self._device = device - self._device_info = DeviceInfo(device) + self._device_info = NestDeviceInfo(device) @property def should_poll(self) -> bool: @@ -73,11 +75,11 @@ class SensorBase(SensorEntity): return f"{self._device.name}-{self.device_class}" @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return device specific attributes.""" return self._device_info.device_info - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when entity is added to register update signal handler.""" self.async_on_remove( self._device.add_update_listener(self.async_write_ha_state) @@ -88,23 +90,23 @@ class TemperatureSensor(SensorBase): """Representation of a Temperature Sensor.""" @property - def name(self): + def name(self) -> str: """Return the name of the sensor.""" return f"{self._device_info.device_name} Temperature" @property - def state(self): + def state(self) -> float: """Return the state of the sensor.""" - trait = self._device.traits[TemperatureTrait.NAME] + trait: TemperatureTrait = self._device.traits[TemperatureTrait.NAME] return trait.ambient_temperature_celsius @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the unit of measurement.""" return TEMP_CELSIUS @property - def device_class(self): + def device_class(self) -> str: """Return the class of this device.""" return DEVICE_CLASS_TEMPERATURE @@ -119,22 +121,22 @@ class HumiditySensor(SensorBase): return f"{self._device.name}-humidity" @property - def name(self): + def name(self) -> str: """Return the name of the sensor.""" return f"{self._device_info.device_name} Humidity" @property - def state(self): + def state(self) -> float: """Return the state of the sensor.""" - trait = self._device.traits[HumidityTrait.NAME] + trait: HumidityTrait = self._device.traits[HumidityTrait.NAME] return trait.ambient_humidity_percent @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the unit of measurement.""" return PERCENTAGE @property - def device_class(self): + def device_class(self) -> str: """Return the class of this device.""" return DEVICE_CLASS_HUMIDITY diff --git a/tests/components/nest/device_info_test.py b/tests/components/nest/device_info_test.py index 1561364d348..a0c6973c1d6 100644 --- a/tests/components/nest/device_info_test.py +++ b/tests/components/nest/device_info_test.py @@ -2,7 +2,7 @@ from google_nest_sdm.device import Device -from homeassistant.components.nest.device_info import DeviceInfo +from homeassistant.components.nest.device_info import NestDeviceInfo def test_device_custom_name(): @@ -20,7 +20,7 @@ def test_device_custom_name(): auth=None, ) - device_info = DeviceInfo(device) + device_info = NestDeviceInfo(device) assert device_info.device_name == "My Doorbell" assert device_info.device_model == "Doorbell" assert device_info.device_brand == "Google Nest" @@ -45,7 +45,7 @@ def test_device_name_room(): auth=None, ) - device_info = DeviceInfo(device) + device_info = NestDeviceInfo(device) assert device_info.device_name == "Some Room" assert device_info.device_model == "Doorbell" assert device_info.device_brand == "Google Nest" @@ -64,7 +64,7 @@ def test_device_no_name(): auth=None, ) - device_info = DeviceInfo(device) + device_info = NestDeviceInfo(device) assert device_info.device_name == "Doorbell" assert device_info.device_model == "Doorbell" assert device_info.device_brand == "Google Nest" @@ -91,13 +91,13 @@ def test_device_invalid_type(): auth=None, ) - device_info = DeviceInfo(device) + device_info = NestDeviceInfo(device) assert device_info.device_name == "My Doorbell" - assert device_info.device_model is None + assert device_info.device_model == "Unknown" assert device_info.device_brand == "Google Nest" assert device_info.device_info == { "identifiers": {("nest", "some-device-id")}, "name": "My Doorbell", "manufacturer": "Google Nest", - "model": None, + "model": "Unknown", } diff --git a/tests/components/nest/sensor_sdm_test.py b/tests/components/nest/sensor_sdm_test.py index dfdfd58d546..cc18e8cd3ae 100644 --- a/tests/components/nest/sensor_sdm_test.py +++ b/tests/components/nest/sensor_sdm_test.py @@ -208,5 +208,5 @@ async def test_device_with_unknown_type(hass): device_registry = dr.async_get(hass) device = device_registry.async_get(entry.device_id) assert device.name == "My Sensor" - assert device.model is None + assert device.model == "Unknown" assert device.identifiers == {("nest", "some-device-id")}