Increase static type coverage for nest integration (#53475)
Co-authored-by: Mick Vleeshouwer <mick@imick.nl> Co-authored-by: Franck Nijhof <git@frenck.dev>pull/53534/head
parent
d4c4263730
commit
6376b4be5c
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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}'")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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"])
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
|
|
|
@ -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")}
|
||||
|
|
Loading…
Reference in New Issue