diff --git a/.strict-typing b/.strict-typing index 4087f970e47..062eee858d5 100644 --- a/.strict-typing +++ b/.strict-typing @@ -18,6 +18,7 @@ homeassistant.components.bond.* homeassistant.components.brother.* homeassistant.components.calendar.* homeassistant.components.camera.* +homeassistant.components.canary.* homeassistant.components.cover.* homeassistant.components.device_automation.* homeassistant.components.elgato.* diff --git a/homeassistant/components/canary/__init__.py b/homeassistant/components/canary/__init__.py index c29dfeb1a71..b276fc4ed34 100644 --- a/homeassistant/components/canary/__init__.py +++ b/homeassistant/components/canary/__init__.py @@ -1,9 +1,12 @@ """Support for Canary devices.""" +from __future__ import annotations + from datetime import timedelta import logging +from typing import Final from canary.api import Api -from requests import ConnectTimeout, HTTPError +from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol 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.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.typing import ConfigType from .const import ( CONF_FFMPEG_ARGUMENTS, @@ -23,11 +27,11 @@ from .const import ( ) 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( cv.deprecated(DOMAIN), { @@ -45,10 +49,10 @@ CONFIG_SCHEMA = vol.Schema( 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.""" hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/canary/alarm_control_panel.py b/homeassistant/components/canary/alarm_control_panel.py index f0d6deb477b..4e29c40f49f 100644 --- a/homeassistant/components/canary/alarm_control_panel.py +++ b/homeassistant/components/canary/alarm_control_panel.py @@ -1,7 +1,14 @@ """Support for Canary alarm.""" 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.const import ( @@ -44,29 +51,33 @@ async def async_setup_entry( class CanaryAlarm(CoordinatorEntity, AlarmControlPanelEntity): """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.""" super().__init__(coordinator) - self._location_id = location.location_id - self._location_name = location.name + self._location_id: str = location.location_id + self._location_name: str = location.name @property - def location(self): + def location(self) -> Location: """Return information about the location.""" return self.coordinator.data["locations"][self._location_id] @property - def name(self): + def name(self) -> str: """Return the name of the alarm.""" return self._location_name @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique ID of the alarm.""" return str(self._location_id) @property - def state(self): + def state(self) -> str | None: """Return the state of the device.""" if self.location.is_private: 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 @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, Any]: """Return the state attributes.""" return {"private": self.location.is_private} - def alarm_disarm(self, code=None): + def alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" self.coordinator.canary.set_location_mode( 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.""" 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.""" 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.""" self.coordinator.canary.set_location_mode( self._location_id, LOCATION_MODE_NIGHT diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index ada9d168942..b1725945db2 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -3,17 +3,25 @@ from __future__ import annotations import asyncio 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.tools import IMAGE_JPEG, ImageFrame import voluptuous as vol -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera -from homeassistant.components.ffmpeg import DATA_FFMPEG +from homeassistant.components.camera import ( + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + Camera, +) +from homeassistant.components.ffmpeg import DATA_FFMPEG, FFmpegManager from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv 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.update_coordinator import CoordinatorEntity from homeassistant.util import Throttle @@ -28,11 +36,11 @@ from .const import ( ) 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), - PLATFORM_SCHEMA.extend( + PARENT_PLATFORM_SCHEMA.extend( { vol.Optional( CONF_FFMPEG_ARGUMENTS, default=DEFAULT_FFMPEG_ARGUMENTS @@ -51,10 +59,10 @@ async def async_setup_entry( coordinator: CanaryDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ DATA_COORDINATOR ] - ffmpeg_arguments = entry.options.get( + ffmpeg_arguments: str = entry.options.get( CONF_FFMPEG_ARGUMENTS, DEFAULT_FFMPEG_ARGUMENTS ) - cameras = [] + cameras: list[CanaryCamera] = [] for location_id, location in coordinator.data["locations"].items(): for device in location.devices: @@ -76,37 +84,47 @@ async def async_setup_entry( class CanaryCamera(CoordinatorEntity, 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.""" super().__init__(coordinator) Camera.__init__(self) - self._ffmpeg = hass.data[DATA_FFMPEG] + self._ffmpeg: FFmpegManager = hass.data[DATA_FFMPEG] self._ffmpeg_arguments = ffmpeg_args self._location_id = location_id self._device = device - self._device_id = device.device_id - self._device_name = device.name + self._device_id: str = device.device_id + self._device_name: str = device.name self._device_type_name = device.device_type["name"] self._timeout = timeout - self._live_stream_session = None + self._live_stream_session: LiveStreamSession | None = None @property - def location(self): + def location(self) -> Location: """Return information about the location.""" return self.coordinator.data["locations"][self._location_id] @property - def name(self): + def name(self) -> str: """Return the name of this device.""" return self._device_name @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique ID of this camera.""" return str(self._device_id) @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device_info of the device.""" return { "identifiers": {(DOMAIN, str(self._device_id))}, @@ -116,16 +134,16 @@ class CanaryCamera(CoordinatorEntity, Camera): } @property - def is_recording(self): + def is_recording(self) -> bool: """Return true if the device is recording.""" - return self.location.is_recording + return self.location.is_recording # type: ignore[no-any-return] @property - def motion_detection_enabled(self): + def motion_detection_enabled(self) -> bool: """Return the camera motion detection status.""" 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.""" await self.hass.async_add_executor_job(self.renew_live_stream_session) live_stream_url = await self.hass.async_add_executor_job( @@ -133,7 +151,7 @@ class CanaryCamera(CoordinatorEntity, Camera): ) ffmpeg = ImageFrame(self._ffmpeg.binary) - image = await asyncio.shield( + image: bytes | None = await asyncio.shield( ffmpeg.get_image( live_stream_url, output_format=IMAGE_JPEG, @@ -142,10 +160,12 @@ class CanaryCamera(CoordinatorEntity, Camera): ) 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.""" if self._live_stream_session is None: - return + return None stream = CameraMjpeg(self._ffmpeg.binary) await stream.open_camera( @@ -164,7 +184,7 @@ class CanaryCamera(CoordinatorEntity, Camera): await stream.close() @Throttle(MIN_TIME_BETWEEN_SESSION_RENEW) - def renew_live_stream_session(self): + def renew_live_stream_session(self) -> None: """Renew live stream session.""" self._live_stream_session = self.coordinator.canary.get_live_stream_session( self._device diff --git a/homeassistant/components/canary/config_flow.py b/homeassistant/components/canary/config_flow.py index f54ae3c308e..ac779a9cb69 100644 --- a/homeassistant/components/canary/config_flow.py +++ b/homeassistant/components/canary/config_flow.py @@ -2,13 +2,13 @@ from __future__ import annotations import logging -from typing import Any +from typing import Final from canary.api import Api -from requests import ConnectTimeout, HTTPError +from requests.exceptions import ConnectTimeout, HTTPError 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.core import HomeAssistant, callback from homeassistant.data_entry_flow import FlowResult @@ -21,10 +21,10 @@ from .const import ( 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. Data has the keys from DATA_SCHEMA with values provided by the user. @@ -46,7 +46,7 @@ class CanaryConfigFlow(ConfigFlow, domain=DOMAIN): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow: """Get the options flow for this handler.""" return CanaryOptionsFlowHandler(config_entry) @@ -100,11 +100,11 @@ class CanaryConfigFlow(ConfigFlow, domain=DOMAIN): class CanaryOptionsFlowHandler(OptionsFlow): """Handle Canary client options.""" - def __init__(self, config_entry): + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize options flow.""" 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.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/homeassistant/components/canary/const.py b/homeassistant/components/canary/const.py index 8219a485ef9..210da35c7c1 100644 --- a/homeassistant/components/canary/const.py +++ b/homeassistant/components/canary/const.py @@ -1,16 +1,18 @@ """Constants for the Canary integration.""" -DOMAIN = "canary" +from typing import Final -MANUFACTURER = "Canary Connect, Inc" +DOMAIN: Final = "canary" + +MANUFACTURER: Final = "Canary Connect, Inc" # Configuration -CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments" +CONF_FFMPEG_ARGUMENTS: Final = "ffmpeg_arguments" # Data -DATA_COORDINATOR = "coordinator" -DATA_UNDO_UPDATE_LISTENER = "undo_update_listener" +DATA_COORDINATOR: Final = "coordinator" +DATA_UNDO_UPDATE_LISTENER: Final = "undo_update_listener" # Defaults -DEFAULT_FFMPEG_ARGUMENTS = "-pred 1" -DEFAULT_TIMEOUT = 10 +DEFAULT_FFMPEG_ARGUMENTS: Final = "-pred 1" +DEFAULT_TIMEOUT: Final = 10 diff --git a/homeassistant/components/canary/coordinator.py b/homeassistant/components/canary/coordinator.py index d1bdac5eee9..4c6c9ce5777 100644 --- a/homeassistant/components/canary/coordinator.py +++ b/homeassistant/components/canary/coordinator.py @@ -1,15 +1,19 @@ """Provides the Canary DataUpdateCoordinator.""" +from __future__ import annotations + +from collections.abc import ValuesView from datetime import timedelta import logging from async_timeout import timeout -from canary.api import Api -from requests import ConnectTimeout, HTTPError +from canary.api import Api, Location +from requests.exceptions import ConnectTimeout, HTTPError from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN +from .model import CanaryData _LOGGER = logging.getLogger(__name__) @@ -29,10 +33,10 @@ class CanaryDataUpdateCoordinator(DataUpdateCoordinator): update_interval=update_interval, ) - def _update_data(self) -> dict: + def _update_data(self) -> CanaryData: """Fetch data from Canary via sync functions.""" - locations_by_id = {} - readings_by_device_id = {} + locations_by_id: dict[str, Location] = {} + readings_by_device_id: dict[str, ValuesView] = {} for location in self.canary.get_locations(): location_id = location.location_id @@ -49,7 +53,7 @@ class CanaryDataUpdateCoordinator(DataUpdateCoordinator): "readings": readings_by_device_id, } - async def _async_update_data(self) -> dict: + async def _async_update_data(self) -> CanaryData: """Fetch data from Canary.""" try: diff --git a/homeassistant/components/canary/model.py b/homeassistant/components/canary/model.py new file mode 100644 index 00000000000..35c6a61a835 --- /dev/null +++ b/homeassistant/components/canary/model.py @@ -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]] diff --git a/homeassistant/components/canary/sensor.py b/homeassistant/components/canary/sensor.py index 0378d34a989..91dc3bad5eb 100644 --- a/homeassistant/components/canary/sensor.py +++ b/homeassistant/components/canary/sensor.py @@ -1,7 +1,9 @@ """Support for Canary sensors.""" 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.config_entries import ConfigEntry @@ -15,41 +17,43 @@ from homeassistant.const import ( TEMP_CELSIUS, ) from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DATA_COORDINATOR, DOMAIN, MANUFACTURER from .coordinator import CanaryDataUpdateCoordinator +from .model import SensorTypeItem -SENSOR_VALUE_PRECISION = 2 -ATTR_AIR_QUALITY = "air_quality" +SENSOR_VALUE_PRECISION: Final = 2 +ATTR_AIR_QUALITY: Final = "air_quality" # 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), # then these variables will need updating, otherwise the sensors will stop working # and disappear in Home Assistant. -CANARY_PRO = "Canary Pro" -CANARY_FLEX = "Canary Flex" +CANARY_PRO: Final = "Canary Pro" +CANARY_FLEX: Final = "Canary Flex" # Sensor types are defined like so: # sensor type name, unit_of_measurement, icon, device class, products supported -SENSOR_TYPES = [ - ["temperature", TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE, [CANARY_PRO]], - ["humidity", PERCENTAGE, None, DEVICE_CLASS_HUMIDITY, [CANARY_PRO]], - ["air_quality", None, "mdi:weather-windy", None, [CANARY_PRO]], - [ +SENSOR_TYPES: Final[list[SensorTypeItem]] = [ + ("temperature", TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE, [CANARY_PRO]), + ("humidity", PERCENTAGE, None, DEVICE_CLASS_HUMIDITY, [CANARY_PRO]), + ("air_quality", None, "mdi:weather-windy", None, [CANARY_PRO]), + ( "wifi", SIGNAL_STRENGTH_DECIBELS_MILLIWATT, None, DEVICE_CLASS_SIGNAL_STRENGTH, [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_ABNORMAL = "abnormal" -STATE_AIR_QUALITY_VERY_ABNORMAL = "very_abnormal" +STATE_AIR_QUALITY_NORMAL: Final = "normal" +STATE_AIR_QUALITY_ABNORMAL: Final = "abnormal" +STATE_AIR_QUALITY_VERY_ABNORMAL: Final = "very_abnormal" async def async_setup_entry( @@ -61,7 +65,7 @@ async def async_setup_entry( coordinator: CanaryDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][ DATA_COORDINATOR ] - sensors = [] + sensors: list[CanarySensor] = [] for location in coordinator.data["locations"].values(): for device in location.devices: @@ -79,8 +83,17 @@ async def async_setup_entry( class CanarySensor(CoordinatorEntity, SensorEntity): """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.""" + super().__init__(coordinator) self._sensor_type = sensor_type self._device_id = device.device_id @@ -105,7 +118,7 @@ class CanarySensor(CoordinatorEntity, SensorEntity): self._canary_type = canary_sensor_type @property - def reading(self): + def reading(self) -> float | None: """Return the device sensor reading.""" readings = self.coordinator.data["readings"][self._device_id] @@ -124,22 +137,22 @@ class CanarySensor(CoordinatorEntity, SensorEntity): return None @property - def name(self): + def name(self) -> str: """Return the name of the Canary sensor.""" return self._name @property - def state(self): + def state(self) -> float | None: """Return the state of the sensor.""" return self.reading @property - def unique_id(self): + def unique_id(self) -> str: """Return the unique ID of this sensor.""" return f"{self._device_id}_{self._sensor_type[0]}" @property - def device_info(self): + def device_info(self) -> DeviceInfo: """Return the device_info of the device.""" return { "identifiers": {(DOMAIN, str(self._device_id))}, @@ -149,22 +162,22 @@ class CanarySensor(CoordinatorEntity, SensorEntity): } @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str | None: """Return the unit of measurement.""" return self._sensor_type[1] @property - def device_class(self): + def device_class(self) -> str | None: """Device class for the sensor.""" return self._sensor_type[3] @property - def icon(self): + def icon(self) -> str | None: """Icon for the sensor.""" return self._sensor_type[2] @property - def extra_state_attributes(self): + def extra_state_attributes(self) -> dict[str, str] | None: """Return the state attributes.""" reading = self.reading @@ -174,7 +187,7 @@ class CanarySensor(CoordinatorEntity, SensorEntity): air_quality = STATE_AIR_QUALITY_VERY_ABNORMAL elif reading <= 0.59: air_quality = STATE_AIR_QUALITY_ABNORMAL - elif reading <= 1.0: + else: air_quality = STATE_AIR_QUALITY_NORMAL return {ATTR_AIR_QUALITY: air_quality} diff --git a/mypy.ini b/mypy.ini index c2af1b1f643..ae8d4d7fa63 100644 --- a/mypy.ini +++ b/mypy.ini @@ -209,6 +209,17 @@ no_implicit_optional = true warn_return_any = 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.*] check_untyped_defs = true disallow_incomplete_defs = true @@ -799,9 +810,6 @@ ignore_errors = true [mypy-homeassistant.components.bsblan.*] ignore_errors = true -[mypy-homeassistant.components.canary.*] -ignore_errors = true - [mypy-homeassistant.components.cast.*] ignore_errors = true diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 3372750b507..743c17088c6 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -31,7 +31,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.bluetooth_tracker.*", "homeassistant.components.bmw_connected_drive.*", "homeassistant.components.bsblan.*", - "homeassistant.components.canary.*", "homeassistant.components.cast.*", "homeassistant.components.cert_expiry.*", "homeassistant.components.climacell.*",