Add strict type annotations to canary (#50943)

* Add strict type annotations

* Add missing futur import

* Apply suggestions

* Apply suggestions
This commit is contained in:
Michael 2021-05-22 10:14:59 +02:00 committed by GitHub
parent 15e2c6d7dc
commit 2e316f6fd5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 175 additions and 95 deletions

View file

@ -18,6 +18,7 @@ homeassistant.components.bond.*
homeassistant.components.brother.* homeassistant.components.brother.*
homeassistant.components.calendar.* homeassistant.components.calendar.*
homeassistant.components.camera.* homeassistant.components.camera.*
homeassistant.components.canary.*
homeassistant.components.cover.* homeassistant.components.cover.*
homeassistant.components.device_automation.* homeassistant.components.device_automation.*
homeassistant.components.elgato.* homeassistant.components.elgato.*

View file

@ -1,9 +1,12 @@
"""Support for Canary devices.""" """Support for Canary devices."""
from __future__ import annotations
from datetime import timedelta from datetime import timedelta
import logging import logging
from typing import Final
from canary.api import Api from canary.api import Api
from requests import ConnectTimeout, HTTPError from requests.exceptions import ConnectTimeout, HTTPError
import voluptuous as vol import voluptuous as vol
from homeassistant.components.camera.const import DOMAIN as CAMERA_DOMAIN from homeassistant.components.camera.const import DOMAIN as CAMERA_DOMAIN
@ -12,6 +15,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType
from .const import ( from .const import (
CONF_FFMPEG_ARGUMENTS, CONF_FFMPEG_ARGUMENTS,
@ -23,11 +27,11 @@ from .const import (
) )
from .coordinator import CanaryDataUpdateCoordinator from .coordinator import CanaryDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER: Final = logging.getLogger(__name__)
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) MIN_TIME_BETWEEN_UPDATES: Final = timedelta(seconds=30)
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA: Final = vol.Schema(
vol.All( vol.All(
cv.deprecated(DOMAIN), cv.deprecated(DOMAIN),
{ {
@ -45,10 +49,10 @@ CONFIG_SCHEMA = vol.Schema(
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
) )
PLATFORMS = ["alarm_control_panel", "camera", "sensor"] PLATFORMS: Final[list[str]] = ["alarm_control_panel", "camera", "sensor"]
async def async_setup(hass: HomeAssistant, config: dict) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Canary integration.""" """Set up the Canary integration."""
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})

View file

@ -1,7 +1,14 @@
"""Support for Canary alarm.""" """Support for Canary alarm."""
from __future__ import annotations from __future__ import annotations
from canary.api import LOCATION_MODE_AWAY, LOCATION_MODE_HOME, LOCATION_MODE_NIGHT from typing import Any
from canary.api import (
LOCATION_MODE_AWAY,
LOCATION_MODE_HOME,
LOCATION_MODE_NIGHT,
Location,
)
from homeassistant.components.alarm_control_panel import AlarmControlPanelEntity from homeassistant.components.alarm_control_panel import AlarmControlPanelEntity
from homeassistant.components.alarm_control_panel.const import ( from homeassistant.components.alarm_control_panel.const import (
@ -44,29 +51,33 @@ async def async_setup_entry(
class CanaryAlarm(CoordinatorEntity, AlarmControlPanelEntity): class CanaryAlarm(CoordinatorEntity, AlarmControlPanelEntity):
"""Representation of a Canary alarm control panel.""" """Representation of a Canary alarm control panel."""
def __init__(self, coordinator, location): coordinator: CanaryDataUpdateCoordinator
def __init__(
self, coordinator: CanaryDataUpdateCoordinator, location: Location
) -> None:
"""Initialize a Canary security camera.""" """Initialize a Canary security camera."""
super().__init__(coordinator) super().__init__(coordinator)
self._location_id = location.location_id self._location_id: str = location.location_id
self._location_name = location.name self._location_name: str = location.name
@property @property
def location(self): def location(self) -> Location:
"""Return information about the location.""" """Return information about the location."""
return self.coordinator.data["locations"][self._location_id] return self.coordinator.data["locations"][self._location_id]
@property @property
def name(self): def name(self) -> str:
"""Return the name of the alarm.""" """Return the name of the alarm."""
return self._location_name return self._location_name
@property @property
def unique_id(self): def unique_id(self) -> str:
"""Return the unique ID of the alarm.""" """Return the unique ID of the alarm."""
return str(self._location_id) return str(self._location_id)
@property @property
def state(self): def state(self) -> str | None:
"""Return the state of the device.""" """Return the state of the device."""
if self.location.is_private: if self.location.is_private:
return STATE_ALARM_DISARMED return STATE_ALARM_DISARMED
@ -87,25 +98,25 @@ class CanaryAlarm(CoordinatorEntity, AlarmControlPanelEntity):
return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT
@property @property
def extra_state_attributes(self): def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes.""" """Return the state attributes."""
return {"private": self.location.is_private} return {"private": self.location.is_private}
def alarm_disarm(self, code=None): def alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command.""" """Send disarm command."""
self.coordinator.canary.set_location_mode( self.coordinator.canary.set_location_mode(
self._location_id, self.location.mode.name, True self._location_id, self.location.mode.name, True
) )
def alarm_arm_home(self, code=None): def alarm_arm_home(self, code: str | None = None) -> None:
"""Send arm home command.""" """Send arm home command."""
self.coordinator.canary.set_location_mode(self._location_id, LOCATION_MODE_HOME) self.coordinator.canary.set_location_mode(self._location_id, LOCATION_MODE_HOME)
def alarm_arm_away(self, code=None): def alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm away command.""" """Send arm away command."""
self.coordinator.canary.set_location_mode(self._location_id, LOCATION_MODE_AWAY) self.coordinator.canary.set_location_mode(self._location_id, LOCATION_MODE_AWAY)
def alarm_arm_night(self, code=None): def alarm_arm_night(self, code: str | None = None) -> None:
"""Send arm night command.""" """Send arm night command."""
self.coordinator.canary.set_location_mode( self.coordinator.canary.set_location_mode(
self._location_id, LOCATION_MODE_NIGHT self._location_id, LOCATION_MODE_NIGHT

View file

@ -3,17 +3,25 @@ from __future__ import annotations
import asyncio import asyncio
from datetime import timedelta from datetime import timedelta
from typing import Final
from aiohttp.web import Request, StreamResponse
from canary.api import Device, Location
from canary.live_stream_api import LiveStreamSession
from haffmpeg.camera import CameraMjpeg from haffmpeg.camera import CameraMjpeg
from haffmpeg.tools import IMAGE_JPEG, ImageFrame from haffmpeg.tools import IMAGE_JPEG, ImageFrame
import voluptuous as vol import voluptuous as vol
from homeassistant.components.camera import PLATFORM_SCHEMA, Camera from homeassistant.components.camera import (
from homeassistant.components.ffmpeg import DATA_FFMPEG PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA,
Camera,
)
from homeassistant.components.ffmpeg import DATA_FFMPEG, FFmpegManager
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream from homeassistant.helpers.aiohttp_client import async_aiohttp_proxy_stream
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import Throttle from homeassistant.util import Throttle
@ -28,11 +36,11 @@ from .const import (
) )
from .coordinator import CanaryDataUpdateCoordinator from .coordinator import CanaryDataUpdateCoordinator
MIN_TIME_BETWEEN_SESSION_RENEW = timedelta(seconds=90) MIN_TIME_BETWEEN_SESSION_RENEW: Final = timedelta(seconds=90)
PLATFORM_SCHEMA = vol.All( PLATFORM_SCHEMA: Final = vol.All(
cv.deprecated(CONF_FFMPEG_ARGUMENTS), cv.deprecated(CONF_FFMPEG_ARGUMENTS),
PLATFORM_SCHEMA.extend( PARENT_PLATFORM_SCHEMA.extend(
{ {
vol.Optional( vol.Optional(
CONF_FFMPEG_ARGUMENTS, default=DEFAULT_FFMPEG_ARGUMENTS CONF_FFMPEG_ARGUMENTS, default=DEFAULT_FFMPEG_ARGUMENTS
@ -51,10 +59,10 @@ async def async_setup_entry(
coordinator: CanaryDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ coordinator: CanaryDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
DATA_COORDINATOR DATA_COORDINATOR
] ]
ffmpeg_arguments = entry.options.get( ffmpeg_arguments: str = entry.options.get(
CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS
) )
cameras = [] cameras: list[CanaryCamera] = []
for location_id, location in coordinator.data["locations"].items(): for location_id, location in coordinator.data["locations"].items():
for device in location.devices: for device in location.devices:
@ -76,37 +84,47 @@ async def async_setup_entry(
class CanaryCamera(CoordinatorEntity, Camera): class CanaryCamera(CoordinatorEntity, Camera):
"""An implementation of a Canary security camera.""" """An implementation of a Canary security camera."""
def __init__(self, hass, coordinator, location_id, device, timeout, ffmpeg_args): coordinator: CanaryDataUpdateCoordinator
def __init__(
self,
hass: HomeAssistant,
coordinator: CanaryDataUpdateCoordinator,
location_id: str,
device: Device,
timeout: int,
ffmpeg_args: str,
) -> None:
"""Initialize a Canary security camera.""" """Initialize a Canary security camera."""
super().__init__(coordinator) super().__init__(coordinator)
Camera.__init__(self) Camera.__init__(self)
self._ffmpeg = hass.data[DATA_FFMPEG] self._ffmpeg: FFmpegManager = hass.data[DATA_FFMPEG]
self._ffmpeg_arguments = ffmpeg_args self._ffmpeg_arguments = ffmpeg_args
self._location_id = location_id self._location_id = location_id
self._device = device self._device = device
self._device_id = device.device_id self._device_id: str = device.device_id
self._device_name = device.name self._device_name: str = device.name
self._device_type_name = device.device_type["name"] self._device_type_name = device.device_type["name"]
self._timeout = timeout self._timeout = timeout
self._live_stream_session = None self._live_stream_session: LiveStreamSession | None = None
@property @property
def location(self): def location(self) -> Location:
"""Return information about the location.""" """Return information about the location."""
return self.coordinator.data["locations"][self._location_id] return self.coordinator.data["locations"][self._location_id]
@property @property
def name(self): def name(self) -> str:
"""Return the name of this device.""" """Return the name of this device."""
return self._device_name return self._device_name
@property @property
def unique_id(self): def unique_id(self) -> str:
"""Return the unique ID of this camera.""" """Return the unique ID of this camera."""
return str(self._device_id) return str(self._device_id)
@property @property
def device_info(self): def device_info(self) -> DeviceInfo:
"""Return the device_info of the device.""" """Return the device_info of the device."""
return { return {
"identifiers": {(DOMAIN, str(self._device_id))}, "identifiers": {(DOMAIN, str(self._device_id))},
@ -116,16 +134,16 @@ class CanaryCamera(CoordinatorEntity, Camera):
} }
@property @property
def is_recording(self): def is_recording(self) -> bool:
"""Return true if the device is recording.""" """Return true if the device is recording."""
return self.location.is_recording return self.location.is_recording # type: ignore[no-any-return]
@property @property
def motion_detection_enabled(self): def motion_detection_enabled(self) -> bool:
"""Return the camera motion detection status.""" """Return the camera motion detection status."""
return not self.location.is_recording return not self.location.is_recording
async def async_camera_image(self): async def async_camera_image(self) -> bytes | None:
"""Return a still image response from the camera.""" """Return a still image response from the camera."""
await self.hass.async_add_executor_job(self.renew_live_stream_session) await self.hass.async_add_executor_job(self.renew_live_stream_session)
live_stream_url = await self.hass.async_add_executor_job( live_stream_url = await self.hass.async_add_executor_job(
@ -133,7 +151,7 @@ class CanaryCamera(CoordinatorEntity, Camera):
) )
ffmpeg = ImageFrame(self._ffmpeg.binary) ffmpeg = ImageFrame(self._ffmpeg.binary)
image = await asyncio.shield( image: bytes | None = await asyncio.shield(
ffmpeg.get_image( ffmpeg.get_image(
live_stream_url, live_stream_url,
output_format=IMAGE_JPEG, output_format=IMAGE_JPEG,
@ -142,10 +160,12 @@ class CanaryCamera(CoordinatorEntity, Camera):
) )
return image return image
async def handle_async_mjpeg_stream(self, request): async def handle_async_mjpeg_stream(
self, request: Request
) -> StreamResponse | None:
"""Generate an HTTP MJPEG stream from the camera.""" """Generate an HTTP MJPEG stream from the camera."""
if self._live_stream_session is None: if self._live_stream_session is None:
return return None
stream = CameraMjpeg(self._ffmpeg.binary) stream = CameraMjpeg(self._ffmpeg.binary)
await stream.open_camera( await stream.open_camera(
@ -164,7 +184,7 @@ class CanaryCamera(CoordinatorEntity, Camera):
await stream.close() await stream.close()
@Throttle(MIN_TIME_BETWEEN_SESSION_RENEW) @Throttle(MIN_TIME_BETWEEN_SESSION_RENEW)
def renew_live_stream_session(self): def renew_live_stream_session(self) -> None:
"""Renew live stream session.""" """Renew live stream session."""
self._live_stream_session = self.coordinator.canary.get_live_stream_session( self._live_stream_session = self.coordinator.canary.get_live_stream_session(
self._device self._device

View file

@ -2,13 +2,13 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from typing import Any from typing import Final
from canary.api import Api from canary.api import Api
from requests import ConnectTimeout, HTTPError from requests.exceptions import ConnectTimeout, HTTPError
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, OptionsFlow from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow
from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
@ -21,10 +21,10 @@ from .const import (
DOMAIN, DOMAIN,
) )
_LOGGER = logging.getLogger(__name__) _LOGGER: Final = logging.getLogger(__name__)
def validate_input(hass: HomeAssistant, data: dict) -> dict[str, Any]: def validate_input(hass: HomeAssistant, data: ConfigType) -> bool:
"""Validate the user input allows us to connect. """Validate the user input allows us to connect.
Data has the keys from DATA_SCHEMA with values provided by the user. Data has the keys from DATA_SCHEMA with values provided by the user.
@ -46,7 +46,7 @@ class CanaryConfigFlow(ConfigFlow, domain=DOMAIN):
@staticmethod @staticmethod
@callback @callback
def async_get_options_flow(config_entry): def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
"""Get the options flow for this handler.""" """Get the options flow for this handler."""
return CanaryOptionsFlowHandler(config_entry) return CanaryOptionsFlowHandler(config_entry)
@ -100,11 +100,11 @@ class CanaryConfigFlow(ConfigFlow, domain=DOMAIN):
class CanaryOptionsFlowHandler(OptionsFlow): class CanaryOptionsFlowHandler(OptionsFlow):
"""Handle Canary client options.""" """Handle Canary client options."""
def __init__(self, config_entry): def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize options flow.""" """Initialize options flow."""
self.config_entry = config_entry self.config_entry = config_entry
async def async_step_init(self, user_input: ConfigType | None = None): async def async_step_init(self, user_input: ConfigType | None = None) -> FlowResult:
"""Manage Canary options.""" """Manage Canary options."""
if user_input is not None: if user_input is not None:
return self.async_create_entry(title="", data=user_input) return self.async_create_entry(title="", data=user_input)

View file

@ -1,16 +1,18 @@
"""Constants for the Canary integration.""" """Constants for the Canary integration."""
DOMAIN = "canary" from typing import Final
MANUFACTURER = "Canary Connect, Inc" DOMAIN: Final = "canary"
MANUFACTURER: Final = "Canary Connect, Inc"
# Configuration # Configuration
CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments" CONF_FFMPEG_ARGUMENTS: Final = "ffmpeg_arguments"
# Data # Data
DATA_COORDINATOR = "coordinator" DATA_COORDINATOR: Final = "coordinator"
DATA_UNDO_UPDATE_LISTENER = "undo_update_listener" DATA_UNDO_UPDATE_LISTENER: Final = "undo_update_listener"
# Defaults # Defaults
DEFAULT_FFMPEG_ARGUMENTS = "-pred 1" DEFAULT_FFMPEG_ARGUMENTS: Final = "-pred 1"
DEFAULT_TIMEOUT = 10 DEFAULT_TIMEOUT: Final = 10

View file

@ -1,15 +1,19 @@
"""Provides the Canary DataUpdateCoordinator.""" """Provides the Canary DataUpdateCoordinator."""
from __future__ import annotations
from collections.abc import ValuesView
from datetime import timedelta from datetime import timedelta
import logging import logging
from async_timeout import timeout from async_timeout import timeout
from canary.api import Api from canary.api import Api, Location
from requests import ConnectTimeout, HTTPError from requests.exceptions import ConnectTimeout, HTTPError
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN from .const import DOMAIN
from .model import CanaryData
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -29,10 +33,10 @@ class CanaryDataUpdateCoordinator(DataUpdateCoordinator):
update_interval=update_interval, update_interval=update_interval,
) )
def _update_data(self) -> dict: def _update_data(self) -> CanaryData:
"""Fetch data from Canary via sync functions.""" """Fetch data from Canary via sync functions."""
locations_by_id = {} locations_by_id: dict[str, Location] = {}
readings_by_device_id = {} readings_by_device_id: dict[str, ValuesView] = {}
for location in self.canary.get_locations(): for location in self.canary.get_locations():
location_id = location.location_id location_id = location.location_id
@ -49,7 +53,7 @@ class CanaryDataUpdateCoordinator(DataUpdateCoordinator):
"readings": readings_by_device_id, "readings": readings_by_device_id,
} }
async def _async_update_data(self) -> dict: async def _async_update_data(self) -> CanaryData:
"""Fetch data from Canary.""" """Fetch data from Canary."""
try: try:

View file

@ -0,0 +1,18 @@
"""Constants for the Canary integration."""
from __future__ import annotations
from collections.abc import ValuesView
from typing import List, Optional, Tuple, TypedDict
from canary.api import Location
class CanaryData(TypedDict):
"""TypedDict for Canary Coordinator Data."""
locations: dict[str, Location]
readings: dict[str, ValuesView]
SensorTypeItem = Tuple[str, Optional[str], Optional[str], Optional[str], List[str]]

View file

@ -1,7 +1,9 @@
"""Support for Canary sensors.""" """Support for Canary sensors."""
from __future__ import annotations from __future__ import annotations
from canary.api import SensorType from typing import Final
from canary.api import Device, Location, SensorType
from homeassistant.components.sensor import SensorEntity from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -15,41 +17,43 @@ from homeassistant.const import (
TEMP_CELSIUS, TEMP_CELSIUS,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DATA_COORDINATOR, DOMAIN, MANUFACTURER from .const import DATA_COORDINATOR, DOMAIN, MANUFACTURER
from .coordinator import CanaryDataUpdateCoordinator from .coordinator import CanaryDataUpdateCoordinator
from .model import SensorTypeItem
SENSOR_VALUE_PRECISION = 2 SENSOR_VALUE_PRECISION: Final = 2
ATTR_AIR_QUALITY = "air_quality" ATTR_AIR_QUALITY: Final = "air_quality"
# Define variables to store the device names, as referred to by the Canary API. # Define variables to store the device names, as referred to by the Canary API.
# Note: If Canary change the name of any of their devices (which they have done), # Note: If Canary change the name of any of their devices (which they have done),
# then these variables will need updating, otherwise the sensors will stop working # then these variables will need updating, otherwise the sensors will stop working
# and disappear in Home Assistant. # and disappear in Home Assistant.
CANARY_PRO = "Canary Pro" CANARY_PRO: Final = "Canary Pro"
CANARY_FLEX = "Canary Flex" CANARY_FLEX: Final = "Canary Flex"
# Sensor types are defined like so: # Sensor types are defined like so:
# sensor type name, unit_of_measurement, icon, device class, products supported # sensor type name, unit_of_measurement, icon, device class, products supported
SENSOR_TYPES = [ SENSOR_TYPES: Final[list[SensorTypeItem]] = [
["temperature", TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE, [CANARY_PRO]], ("temperature", TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE, [CANARY_PRO]),
["humidity", PERCENTAGE, None, DEVICE_CLASS_HUMIDITY, [CANARY_PRO]], ("humidity", PERCENTAGE, None, DEVICE_CLASS_HUMIDITY, [CANARY_PRO]),
["air_quality", None, "mdi:weather-windy", None, [CANARY_PRO]], ("air_quality", None, "mdi:weather-windy", None, [CANARY_PRO]),
[ (
"wifi", "wifi",
SIGNAL_STRENGTH_DECIBELS_MILLIWATT, SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
None, None,
DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_SIGNAL_STRENGTH,
[CANARY_FLEX], [CANARY_FLEX],
], ),
["battery", PERCENTAGE, None, DEVICE_CLASS_BATTERY, [CANARY_FLEX]], ("battery", PERCENTAGE, None, DEVICE_CLASS_BATTERY, [CANARY_FLEX]),
] ]
STATE_AIR_QUALITY_NORMAL = "normal" STATE_AIR_QUALITY_NORMAL: Final = "normal"
STATE_AIR_QUALITY_ABNORMAL = "abnormal" STATE_AIR_QUALITY_ABNORMAL: Final = "abnormal"
STATE_AIR_QUALITY_VERY_ABNORMAL = "very_abnormal" STATE_AIR_QUALITY_VERY_ABNORMAL: Final = "very_abnormal"
async def async_setup_entry( async def async_setup_entry(
@ -61,7 +65,7 @@ async def async_setup_entry(
coordinator: CanaryDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ coordinator: CanaryDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
DATA_COORDINATOR DATA_COORDINATOR
] ]
sensors = [] sensors: list[CanarySensor] = []
for location in coordinator.data["locations"].values(): for location in coordinator.data["locations"].values():
for device in location.devices: for device in location.devices:
@ -79,8 +83,17 @@ async def async_setup_entry(
class CanarySensor(CoordinatorEntity, SensorEntity): class CanarySensor(CoordinatorEntity, SensorEntity):
"""Representation of a Canary sensor.""" """Representation of a Canary sensor."""
def __init__(self, coordinator, sensor_type, location, device): coordinator: CanaryDataUpdateCoordinator
def __init__(
self,
coordinator: CanaryDataUpdateCoordinator,
sensor_type: SensorTypeItem,
location: Location,
device: Device,
) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(coordinator) super().__init__(coordinator)
self._sensor_type = sensor_type self._sensor_type = sensor_type
self._device_id = device.device_id self._device_id = device.device_id
@ -105,7 +118,7 @@ class CanarySensor(CoordinatorEntity, SensorEntity):
self._canary_type = canary_sensor_type self._canary_type = canary_sensor_type
@property @property
def reading(self): def reading(self) -> float | None:
"""Return the device sensor reading.""" """Return the device sensor reading."""
readings = self.coordinator.data["readings"][self._device_id] readings = self.coordinator.data["readings"][self._device_id]
@ -124,22 +137,22 @@ class CanarySensor(CoordinatorEntity, SensorEntity):
return None return None
@property @property
def name(self): def name(self) -> str:
"""Return the name of the Canary sensor.""" """Return the name of the Canary sensor."""
return self._name return self._name
@property @property
def state(self): def state(self) -> float | None:
"""Return the state of the sensor.""" """Return the state of the sensor."""
return self.reading return self.reading
@property @property
def unique_id(self): def unique_id(self) -> str:
"""Return the unique ID of this sensor.""" """Return the unique ID of this sensor."""
return f"{self._device_id}_{self._sensor_type[0]}" return f"{self._device_id}_{self._sensor_type[0]}"
@property @property
def device_info(self): def device_info(self) -> DeviceInfo:
"""Return the device_info of the device.""" """Return the device_info of the device."""
return { return {
"identifiers": {(DOMAIN, str(self._device_id))}, "identifiers": {(DOMAIN, str(self._device_id))},
@ -149,22 +162,22 @@ class CanarySensor(CoordinatorEntity, SensorEntity):
} }
@property @property
def unit_of_measurement(self): def unit_of_measurement(self) -> str | None:
"""Return the unit of measurement.""" """Return the unit of measurement."""
return self._sensor_type[1] return self._sensor_type[1]
@property @property
def device_class(self): def device_class(self) -> str | None:
"""Device class for the sensor.""" """Device class for the sensor."""
return self._sensor_type[3] return self._sensor_type[3]
@property @property
def icon(self): def icon(self) -> str | None:
"""Icon for the sensor.""" """Icon for the sensor."""
return self._sensor_type[2] return self._sensor_type[2]
@property @property
def extra_state_attributes(self): def extra_state_attributes(self) -> dict[str, str] | None:
"""Return the state attributes.""" """Return the state attributes."""
reading = self.reading reading = self.reading
@ -174,7 +187,7 @@ class CanarySensor(CoordinatorEntity, SensorEntity):
air_quality = STATE_AIR_QUALITY_VERY_ABNORMAL air_quality = STATE_AIR_QUALITY_VERY_ABNORMAL
elif reading <= 0.59: elif reading <= 0.59:
air_quality = STATE_AIR_QUALITY_ABNORMAL air_quality = STATE_AIR_QUALITY_ABNORMAL
elif reading <= 1.0: else:
air_quality = STATE_AIR_QUALITY_NORMAL air_quality = STATE_AIR_QUALITY_NORMAL
return {ATTR_AIR_QUALITY: air_quality} return {ATTR_AIR_QUALITY: air_quality}

View file

@ -209,6 +209,17 @@ no_implicit_optional = true
warn_return_any = true warn_return_any = true
warn_unreachable = true warn_unreachable = true
[mypy-homeassistant.components.canary.*]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.cover.*] [mypy-homeassistant.components.cover.*]
check_untyped_defs = true check_untyped_defs = true
disallow_incomplete_defs = true disallow_incomplete_defs = true
@ -799,9 +810,6 @@ ignore_errors = true
[mypy-homeassistant.components.bsblan.*] [mypy-homeassistant.components.bsblan.*]
ignore_errors = true ignore_errors = true
[mypy-homeassistant.components.canary.*]
ignore_errors = true
[mypy-homeassistant.components.cast.*] [mypy-homeassistant.components.cast.*]
ignore_errors = true ignore_errors = true

View file

@ -31,7 +31,6 @@ IGNORED_MODULES: Final[list[str]] = [
"homeassistant.components.bluetooth_tracker.*", "homeassistant.components.bluetooth_tracker.*",
"homeassistant.components.bmw_connected_drive.*", "homeassistant.components.bmw_connected_drive.*",
"homeassistant.components.bsblan.*", "homeassistant.components.bsblan.*",
"homeassistant.components.canary.*",
"homeassistant.components.cast.*", "homeassistant.components.cast.*",
"homeassistant.components.cert_expiry.*", "homeassistant.components.cert_expiry.*",
"homeassistant.components.climacell.*", "homeassistant.components.climacell.*",