From 6f3e7c67e9a4189fe61b36836410eb9b4e972119 Mon Sep 17 00:00:00 2001 From: Andrey Khrolenok Date: Thu, 4 Mar 2021 04:12:27 +0300 Subject: [PATCH] Update component files stucture --- custom_components/gismeteo/__init__.py | 181 ------- custom_components/gismeteo/config_flow.py | 102 ---- custom_components/gismeteo/const.py | 188 ------- custom_components/gismeteo/entity.py | 45 ++ custom_components/gismeteo/gismeteo.py | 480 ------------------ custom_components/gismeteo/manifest.json | 15 - custom_components/gismeteo/sensor.py | 52 +- custom_components/gismeteo/weather.py | 56 +- .../integration_blueprint/entity.py | 38 -- tests/__init__.py | 10 +- tests/test_api.py | 4 +- tests/test_config_flow.py | 8 +- tests/test_gismeteo.py | 2 +- tests/test_init.py | 14 +- tests/test_sensor.py | 4 +- tests/test_switch.py | 4 +- tests/test_weather.py | 4 +- 17 files changed, 104 insertions(+), 1103 deletions(-) delete mode 100644 custom_components/gismeteo/__init__.py delete mode 100644 custom_components/gismeteo/config_flow.py delete mode 100644 custom_components/gismeteo/const.py create mode 100644 custom_components/gismeteo/entity.py delete mode 100644 custom_components/gismeteo/gismeteo.py delete mode 100644 custom_components/gismeteo/manifest.json delete mode 100644 custom_components/integration_blueprint/entity.py diff --git a/custom_components/gismeteo/__init__.py b/custom_components/gismeteo/__init__.py deleted file mode 100644 index 696345a..0000000 --- a/custom_components/gismeteo/__init__.py +++ /dev/null @@ -1,181 +0,0 @@ -# -# Copyright (c) 2019-2021, Andrey "Limych" Khrolenok -# Creative Commons BY-NC-SA 4.0 International Public License -# (see LICENSE.md or https://creativecommons.org/licenses/by-nc-sa/4.0/) -# -""" -The Gismeteo component. - -For more details about this platform, please refer to the documentation at -https://github.com/Limych/ha-gismeteo/ -""" - -import asyncio -import json -import logging -import os - -from aiohttp import ClientConnectorError -from async_timeout import timeout -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, CONF_PLATFORM -from homeassistant.core import Config, HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.storage import STORAGE_DIR -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed - -from .const import ( - CONF_CACHE_DIR, - CONF_PLATFORMS, - CONF_YAML, - COORDINATOR, - FORECAST_MODE_HOURLY, - UNDO_UPDATE_LISTENER, - UPDATE_INTERVAL, -) -from .gismeteo import ApiError, Gismeteo - -_LOGGER = logging.getLogger(__name__) - -# Base component constants -DOMAIN = "gismeteo" -ATTRIBUTION = "Data provided by Gismeteo" - -PLATFORMS = [SENSOR_DOMAIN, WEATHER_DOMAIN] - - -# pylint: disable=unused-argument -async def async_setup(hass: HomeAssistant, config: Config) -> bool: - """Set up component.""" - # Print startup messages - with open(os.path.dirname(os.path.abspath(__file__)) + "/manifest.json") as file: - manifest = json.load(file) - _LOGGER.info("%s v%s", manifest["name"], manifest["version"]) - _LOGGER.info( - "If you have ANY issues with this component, please report them here: %s", - manifest["issue_tracker"], - ) - - hass.data.setdefault(DOMAIN, {}) - - for entry in hass.config_entries.async_entries(DOMAIN): - if entry.source == "import": - await hass.config_entries.async_remove(entry.entry_id) - - return True - - -def get_gismeteo(hass: HomeAssistant, config): - """Prepare Gismeteo instance.""" - return Gismeteo( - async_get_clientsession(hass), - latitude=config.get(CONF_LATITUDE, hass.config.latitude), - longitude=config.get(CONF_LONGITUDE, hass.config.longitude), - mode=config.get(CONF_MODE, FORECAST_MODE_HOURLY), - params={ - "timezone": str(hass.config.time_zone), - "cache_dir": config.get(CONF_CACHE_DIR, hass.config.path(STORAGE_DIR)), - "cache_time": UPDATE_INTERVAL.total_seconds(), - }, - ) - - -async def _async_get_coordinator(hass: HomeAssistant, config: dict): - """Prepare update coordinator instance.""" - gismeteo = get_gismeteo(hass, config) - await gismeteo.async_get_location() - - coordinator = GismeteoDataUpdateCoordinator(hass, gismeteo) - await coordinator.async_refresh() - - if not coordinator.last_update_success: - raise ConfigEntryNotReady - - return coordinator - - -async def async_setup_entry(hass: HomeAssistant, config_entry) -> bool: - """Set up Gismeteo as config entry.""" - if config_entry.source == "import": - # Setup from configuration.yaml - await asyncio.sleep(12) - - platforms = set() - - for uid, cfg in hass.data[DOMAIN][CONF_YAML].items(): - platforms.add(cfg[CONF_PLATFORM]) - coordinator = await _async_get_coordinator(hass, cfg) - hass.data[DOMAIN][uid] = { - COORDINATOR: coordinator, - } - - undo_listener = config_entry.add_update_listener(update_listener) - hass.data[DOMAIN][config_entry.entry_id] = { - UNDO_UPDATE_LISTENER: undo_listener, - } - platforms = list(platforms) - - else: - # Setup from config entry - platforms = config_entry.data.get(CONF_PLATFORMS, PLATFORMS) - - coordinator = await _async_get_coordinator(hass, config_entry.data) - undo_listener = config_entry.add_update_listener(update_listener) - hass.data[DOMAIN][config_entry.entry_id] = { - COORDINATOR: coordinator, - UNDO_UPDATE_LISTENER: undo_listener, - } - - for component in platforms: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, component) - ) - - return True - - -async def async_unload_entry(hass, config_entry): - """Unload a config entry.""" - platforms = config_entry.data.get(CONF_PLATFORMS, [SENSOR_DOMAIN, WEATHER_DOMAIN]) - - unload_ok = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(config_entry, component) - for component in platforms - ] - ) - ) - - hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]() - - if unload_ok: - hass.data[DOMAIN].pop(config_entry.entry_id) - - return unload_ok - - -async def update_listener(hass, config_entry): - """Update listener.""" - await hass.config_entries.async_reload(config_entry.entry_id) - - -class GismeteoDataUpdateCoordinator(DataUpdateCoordinator): - """Class to manage fetching Gismeteo data API.""" - - def __init__(self, hass: HomeAssistant, gismeteo: Gismeteo): - """Initialize.""" - self.gismeteo = gismeteo - - super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL) - - async def _async_update_data(self): - """Update data via library.""" - try: - async with timeout(10): - await self.gismeteo.async_update() - return self.gismeteo.current - except (ApiError, ClientConnectorError) as error: - raise UpdateFailed(error) from error diff --git a/custom_components/gismeteo/config_flow.py b/custom_components/gismeteo/config_flow.py deleted file mode 100644 index 4a2603d..0000000 --- a/custom_components/gismeteo/config_flow.py +++ /dev/null @@ -1,102 +0,0 @@ -# -# Copyright (c) 2019-2021, Andrey "Limych" Khrolenok -# Creative Commons BY-NC-SA 4.0 International Public License -# (see LICENSE.md or https://creativecommons.org/licenses/by-nc-sa/4.0/) -# -""" -The Gismeteo component. - -For more details about this platform, please refer to the documentation at -https://github.com/Limych/ha-gismeteo/ -""" -import asyncio -import logging - -from aiohttp import ClientConnectorError, ClientError -from async_timeout import timeout -from homeassistant import config_entries -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE, CONF_NAME -import homeassistant.helpers.config_validation as cv -import voluptuous as vol - -from . import DOMAIN, get_gismeteo # pylint: disable=unused-import -from .const import ( - CONF_FORECAST, - CONF_PLATFORMS, - FORECAST_MODE_DAILY, - FORECAST_MODE_HOURLY, -) -from .gismeteo import ApiError - -_LOGGER = logging.getLogger(__name__) - - -class GismeteoFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): - """Config flow for Gismeteo.""" - - VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL - - async def async_step_import(self, platform_config): - """Import a config entry. - - Special type of import, we're not actually going to store any data. - Instead, we're going to rely on the values that are in config file. - """ - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - - return self.async_create_entry(title="configuration.yaml", data=platform_config) - - async def async_step_user(self, user_input=None): - """Handle a flow initialized by the user.""" - if self._async_current_entries(): - return self.async_abort(reason="single_instance_allowed") - - errors = {} - - if user_input is not None: - platforms = user_input.get(CONF_PLATFORMS, [SENSOR_DOMAIN, WEATHER_DOMAIN]) - user_input[CONF_PLATFORMS] = platforms - - try: - async with timeout(10): - gismeteo = get_gismeteo(self.hass, user_input) - await gismeteo.async_update() - except (ApiError, ClientConnectorError, asyncio.TimeoutError, ClientError): - errors["base"] = "cannot_connect" - else: - await self.async_set_unique_id( - gismeteo.unique_id, raise_on_progress=False - ) - - return self.async_create_entry( - title=user_input[CONF_NAME], data=user_input - ) - - return self.async_show_form( - step_id="user", - data_schema=vol.Schema( - { - vol.Optional( - CONF_LATITUDE, default=self.hass.config.latitude - ): cv.latitude, - vol.Optional( - CONF_LONGITUDE, default=self.hass.config.longitude - ): cv.longitude, - vol.Optional( - CONF_NAME, default=self.hass.config.location_name - ): str, - vol.Optional( - CONF_FORECAST, - default=self.hass.config.get(CONF_FORECAST, False), - ): bool, - vol.Optional(CONF_MODE, default=FORECAST_MODE_HOURLY): vol.In( - [FORECAST_MODE_HOURLY, FORECAST_MODE_DAILY] - ), - } - ), - errors=errors, - ) diff --git a/custom_components/gismeteo/const.py b/custom_components/gismeteo/const.py deleted file mode 100644 index cba698e..0000000 --- a/custom_components/gismeteo/const.py +++ /dev/null @@ -1,188 +0,0 @@ -# -# Copyright (c) 2019-2020, Andrey "Limych" Khrolenok -# Creative Commons BY-NC-SA 4.0 International Public License -# (see LICENSE.md or https://creativecommons.org/licenses/by-nc-sa/4.0/) -# -""" -The Gismeteo component. - -For more details about this platform, please refer to the documentation at -https://github.com/Limych/ha-gismeteo/ -""" - -from datetime import timedelta - -from homeassistant.components.weather import ATTR_FORECAST_CONDITION -from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_ICON, - ATTR_NAME, - ATTR_UNIT_OF_MEASUREMENT, - DEGREE, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, - LENGTH_MILLIMETERS, - PRESSURE_HPA, - SPEED_METERS_PER_SECOND, - TEMP_CELSIUS, -) - -try: - from homeassistant.const import PERCENTAGE -except ImportError: # pragma: no cover - from homeassistant.const import UNIT_PERCENTAGE as PERCENTAGE - -ENDPOINT_URL = "https://services.gismeteo.ru/inform-service/inf_chrome" - -MMHG2HPA = 1.333223684 -MS2KMH = 3.6 - -CONF_CACHE_DIR = "cache_dir" -CONF_FORECAST = "forecast" -CONF_PLATFORMS = "platforms" -CONF_YAML = "_yaml" - -FORECAST_MODE_HOURLY = "hourly" -FORECAST_MODE_DAILY = "daily" - -DEFAULT_NAME = "Gismeteo" - -UPDATE_INTERVAL = timedelta(minutes=5) - -CONDITION_FOG_CLASSES = [ - 11, - 12, - 28, - 40, - 41, - 42, - 43, - 44, - 45, - 46, - 47, - 48, - 49, - 120, - 130, - 131, - 132, - 133, - 134, - 135, - 528, -] - -ATTR_SUNRISE = "sunrise" -ATTR_SUNSET = "sunset" - -ATTR_WEATHER_CONDITION = ATTR_FORECAST_CONDITION -ATTR_WEATHER_CLOUDINESS = "cloudiness" -ATTR_WEATHER_PRECIPITATION_TYPE = "precipitation_type" -ATTR_WEATHER_PRECIPITATION_AMOUNT = "precipitation_amount" -ATTR_WEATHER_PRECIPITATION_INTENSITY = "precipitation_intensity" -ATTR_WEATHER_STORM = "storm" -ATTR_WEATHER_GEOMAGNETIC_FIELD = "gm_field" -ATTR_WEATHER_PHENOMENON = "phenomenon" -ATTR_WEATHER_WATER_TEMPERATURE = "water_temperature" - -ATTR_FORECAST_HUMIDITY = "humidity" -ATTR_FORECAST_PRESSURE = "pressure" -ATTR_FORECAST_CLOUDINESS = ATTR_WEATHER_CLOUDINESS -ATTR_FORECAST_PRECIPITATION_TYPE = ATTR_WEATHER_PRECIPITATION_TYPE -ATTR_FORECAST_PRECIPITATION_AMOUNT = ATTR_WEATHER_PRECIPITATION_AMOUNT -ATTR_FORECAST_PRECIPITATION_INTENSITY = ATTR_WEATHER_PRECIPITATION_INTENSITY -ATTR_FORECAST_STORM = ATTR_WEATHER_STORM -ATTR_FORECAST_GEOMAGNETIC_FIELD = ATTR_WEATHER_GEOMAGNETIC_FIELD -ATTR_FORECAST_PHENOMENON = ATTR_WEATHER_PHENOMENON - -PRECIPITATION_AMOUNT = (0, 2, 6, 16) - -SENSOR_TYPES = { - "weather": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: None, - ATTR_NAME: "Condition", - ATTR_UNIT_OF_MEASUREMENT: None, - }, - "temperature": { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: None, - ATTR_NAME: "Temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, - }, - "wind_speed": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-windy", - ATTR_NAME: "Wind speed", - ATTR_UNIT_OF_MEASUREMENT: SPEED_METERS_PER_SECOND, - }, - "wind_bearing": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-windy", - ATTR_NAME: "Wind bearing", - ATTR_UNIT_OF_MEASUREMENT: DEGREE, - }, - "humidity": { - ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, - ATTR_ICON: None, - ATTR_NAME: "Humidity", - ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, - }, - "pressure": { - ATTR_DEVICE_CLASS: DEVICE_CLASS_PRESSURE, - ATTR_ICON: None, - ATTR_NAME: "Pressure", - ATTR_UNIT_OF_MEASUREMENT: PRESSURE_HPA, - }, - "clouds": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-partly-cloudy", - ATTR_NAME: "Cloud coverage", - ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, - }, - "rain": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-rainy", - ATTR_NAME: "Rain", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_MILLIMETERS, - }, - "snow": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-snowy", - ATTR_NAME: "Snow", - ATTR_UNIT_OF_MEASUREMENT: LENGTH_MILLIMETERS, - }, - "storm": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:weather-lightning", - ATTR_NAME: "Storm", - ATTR_UNIT_OF_MEASUREMENT: None, - }, - "geomagnetic": { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: "mdi:magnet-on", - ATTR_NAME: "Geomagnetic field", - ATTR_UNIT_OF_MEASUREMENT: "", - }, - "water_temperature": { - ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, - ATTR_ICON: None, - ATTR_NAME: "Water Temperature", - ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, - }, -} -FORECAST_SENSOR_TYPE = { - ATTR_DEVICE_CLASS: None, - ATTR_ICON: None, - ATTR_NAME: "Forecast", - ATTR_UNIT_OF_MEASUREMENT: None, -} - -HTTP_HEADERS: dict = {"Content-Encoding": "gzip"} -HTTP_OK: int = 200 - -COORDINATOR = "coordinator" -UNDO_UPDATE_LISTENER = "undo_update_listener" -NAME = "Gismeteo" diff --git a/custom_components/gismeteo/entity.py b/custom_components/gismeteo/entity.py new file mode 100644 index 0000000..36f146f --- /dev/null +++ b/custom_components/gismeteo/entity.py @@ -0,0 +1,45 @@ +# Copyright (c) 2019-2021, Andrey "Limych" Khrolenok +# Creative Commons BY-NC-SA 4.0 International Public License +# (see LICENSE.md or https://creativecommons.org/licenses/by-nc-sa/4.0/) +""" +The Gismeteo component. + +For more details about this platform, please refer to the documentation at +https://github.com/Limych/ha-gismeteo/ +""" + +from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import GismeteoDataUpdateCoordinator +from .const import ATTRIBUTION, DOMAIN, NAME +from .gismeteo import Gismeteo + + +class GismeteoEntity(CoordinatorEntity): + """Gismeteo entity.""" + + def __init__(self, name: str, coordinator: GismeteoDataUpdateCoordinator): + """Class initialization.""" + super().__init__(coordinator) + self._name = name + + @property + def _gismeteo(self) -> Gismeteo: + return self.coordinator.gismeteo + + @property + def device_info(self): + """Return the device info.""" + return { + "identifiers": {(DOMAIN, self._gismeteo.location_key)}, + "name": NAME, + "entry_type": "service", + } + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_ATTRIBUTION: ATTRIBUTION, + } diff --git a/custom_components/gismeteo/gismeteo.py b/custom_components/gismeteo/gismeteo.py deleted file mode 100644 index 14a43fb..0000000 --- a/custom_components/gismeteo/gismeteo.py +++ /dev/null @@ -1,480 +0,0 @@ -# -# Copyright (c) 2019-2021, Andrey "Limych" Khrolenok -# Creative Commons BY-NC-SA 4.0 International Public License -# (see LICENSE.md or https://creativecommons.org/licenses/by-nc-sa/4.0/) -# -""" -The Gismeteo component. - -For more details about this platform, please refer to the documentation at -https://github.com/Limych/ha-gismeteo/ -""" - -from datetime import datetime -import logging -import time -from typing import Any, Callable, Optional - -from aiohttp import ClientSession -import defusedxml.ElementTree as etree # type: ignore -from homeassistant.components.weather import ( - ATTR_CONDITION_CLEAR_NIGHT, - ATTR_CONDITION_CLOUDY, - ATTR_CONDITION_FOG, - ATTR_CONDITION_LIGHTNING, - ATTR_CONDITION_LIGHTNING_RAINY, - ATTR_CONDITION_PARTLYCLOUDY, - ATTR_CONDITION_POURING, - ATTR_CONDITION_RAINY, - ATTR_CONDITION_SNOWY, - ATTR_CONDITION_SNOWY_RAINY, - ATTR_CONDITION_SUNNY, - ATTR_CONDITION_WINDY, - ATTR_CONDITION_WINDY_VARIANT, - ATTR_FORECAST_CONDITION, - ATTR_FORECAST_PRECIPITATION, - ATTR_FORECAST_TEMP, - ATTR_FORECAST_TEMP_LOW, - ATTR_FORECAST_TIME, - ATTR_FORECAST_WIND_BEARING, - ATTR_FORECAST_WIND_SPEED, - ATTR_WEATHER_HUMIDITY, - ATTR_WEATHER_PRESSURE, - ATTR_WEATHER_TEMPERATURE, - ATTR_WEATHER_WIND_BEARING, - ATTR_WEATHER_WIND_SPEED, -) -from homeassistant.const import HTTP_OK, STATE_UNKNOWN -from homeassistant.util import dt as dt_util - -from .cache import Cache -from .const import ( - ATTR_FORECAST_CLOUDINESS, - ATTR_FORECAST_GEOMAGNETIC_FIELD, - ATTR_FORECAST_HUMIDITY, - ATTR_FORECAST_PRECIPITATION_AMOUNT, - ATTR_FORECAST_PRECIPITATION_INTENSITY, - ATTR_FORECAST_PRECIPITATION_TYPE, - ATTR_FORECAST_PRESSURE, - ATTR_FORECAST_STORM, - ATTR_SUNRISE, - ATTR_SUNSET, - ATTR_WEATHER_CLOUDINESS, - ATTR_WEATHER_CONDITION, - ATTR_WEATHER_GEOMAGNETIC_FIELD, - ATTR_WEATHER_PHENOMENON, - ATTR_WEATHER_PRECIPITATION_AMOUNT, - ATTR_WEATHER_PRECIPITATION_INTENSITY, - ATTR_WEATHER_PRECIPITATION_TYPE, - ATTR_WEATHER_STORM, - ATTR_WEATHER_WATER_TEMPERATURE, - CONDITION_FOG_CLASSES, - ENDPOINT_URL, - FORECAST_MODE_DAILY, - FORECAST_MODE_HOURLY, - HTTP_HEADERS, - MMHG2HPA, - MS2KMH, -) - -_LOGGER = logging.getLogger(__name__) - - -class InvalidCoordinatesError(Exception): - """Raised when coordinates are invalid.""" - - def __init__(self, status): - """Initialize.""" - super().__init__(status) - self.status = status - - -class ApiError(Exception): - """Raised when Gismeteo API request ended in error.""" - - def __init__(self, status): - """Initialize.""" - super().__init__(status) - self.status = status - - -class Gismeteo: - """Gismeteo API implementation.""" - - def __init__( - self, - session: ClientSession, - latitude: Optional[float] = None, - longitude: Optional[float] = None, - location_key: Optional[int] = None, - mode=FORECAST_MODE_HOURLY, - params: Optional[dict] = None, - ): - """Initialize.""" - params = params or {} - - if not location_key: - if not self._valid_coordinates(latitude, longitude): - raise InvalidCoordinatesError("Your coordinates are invalid.") - - _LOGGER.debug("Place coordinates: %s, %s", latitude, longitude) - _LOGGER.debug("Forecast mode: %s", mode) - - self._session = session - self._mode = mode - self._cache = Cache(params) if params.get("cache_dir") is not None else None - self._latitude = latitude - self._longitude = longitude - self._location_key = location_key - self._location_name = None - - self._current = {} - self._forecast = [] - self._timezone = ( - dt_util.get_time_zone(params.get("timezone")) - if params.get("timezone") is not None - else dt_util.DEFAULT_TIME_ZONE - ) - - @staticmethod - def _valid_coordinates(latitude: float, longitude: float) -> bool: - """Return True if coordinates are valid.""" - try: - assert isinstance(latitude, (int, float)) and isinstance( - longitude, (int, float) - ) - assert abs(latitude) <= 90 and abs(longitude) <= 180 - except (AssertionError, TypeError): - return False - return True - - @property - def unique_id(self): - """Return a unique_id.""" - return f"{self._location_key}-{self._mode}".lower() - - @property - def current(self): - """Return current weather data.""" - return self._current - - @property - def location_name(self): - """Return location name.""" - return self._location_name - - @property - def location_key(self): - """Return location key.""" - return self._location_key - - async def _async_get_data(self, url: str, cache_fname=None) -> str: - """Retreive data from Gismeteo API and cache results.""" - _LOGGER.debug("Requesting URL %s", url) - - if self._cache and cache_fname is not None: - cache_fname += ".xml" - if self._cache.is_cached(cache_fname): - _LOGGER.debug("Cached response used") - return self._cache.read_cache(cache_fname) - - async with self._session.get(url, headers=HTTP_HEADERS) as resp: - if resp.status != HTTP_OK: - raise ApiError(f"Invalid response from Gismeteo API: {resp.status}") - _LOGGER.debug("Data retrieved from %s, status: %s", url, resp.status) - data = await resp.text() - - if self._cache and cache_fname is not None and data: - self._cache.save_cache(cache_fname, data) - - return data - - async def async_get_location(self): - """Retreive location data from Gismeteo.""" - url = (ENDPOINT_URL + "/cities/?lat={}&lng={}&count=1&lang=en").format( - self._latitude, self._longitude - ) - cache_fname = f"location_{self._latitude}_{self._longitude}" - - response = await self._async_get_data(url, cache_fname) - try: - xml = etree.fromstring(response) - item = xml.find("item") - self._location_key = self._get(item, "id", int) - self._location_name = self._get(item, "n") - except (etree.ParseError, TypeError, AttributeError) as ex: - raise ApiError( - "Can't retrieve location data! Invalid server response." - ) from ex - - @staticmethod - def _get(var: dict, ind: str, func: Optional[Callable] = None) -> Any: - res = var.get(ind) - if func is not None: - try: - res = func(res) - except (TypeError, ValueError, ArithmeticError): - return None - return res - - @staticmethod - def _is_day(testing_time, sunrise_time, sunset_time): - """Return True if sun are shining.""" - return sunrise_time < testing_time < sunset_time - - def condition(self, src=None): - """Return the current condition.""" - src = src or self._current - - cld = src.get(ATTR_WEATHER_CLOUDINESS) - if cld is None: - return None - if cld == 0: - if self._mode == FORECAST_MODE_DAILY or self._is_day( - src.get(ATTR_FORECAST_TIME, time.time()), - src.get(ATTR_SUNRISE), - src.get(ATTR_SUNSET), - ): - cond = ATTR_CONDITION_SUNNY # Sunshine - else: - cond = ATTR_CONDITION_CLEAR_NIGHT # Clear night - elif cld == 1: - cond = ATTR_CONDITION_PARTLYCLOUDY # A few clouds - elif cld == 2: - cond = ATTR_CONDITION_PARTLYCLOUDY # A some clouds - else: - cond = ATTR_CONDITION_CLOUDY # Many clouds - - pr_type = src.get(ATTR_WEATHER_PRECIPITATION_TYPE) - pr_int = src.get(ATTR_WEATHER_PRECIPITATION_INTENSITY) - if src.get(ATTR_WEATHER_STORM): - cond = ATTR_CONDITION_LIGHTNING # Lightning/ thunderstorms - if pr_type != 0: - cond = ( - ATTR_CONDITION_LIGHTNING_RAINY # Lightning/ thunderstorms and rain - ) - elif pr_type == 1: - cond = ATTR_CONDITION_RAINY # Rain - if pr_int == 3: - cond = ATTR_CONDITION_POURING # Pouring rain - elif pr_type == 2: - cond = ATTR_CONDITION_SNOWY # Snow - elif pr_type == 3: - cond = ATTR_CONDITION_SNOWY_RAINY # Snow and Rain - elif self.wind_speed_ms(src) > 10.8: - if cond == ATTR_CONDITION_CLOUDY: - cond = ATTR_CONDITION_WINDY_VARIANT # Wind and clouds - else: - cond = ATTR_CONDITION_WINDY # Wind - elif ( - cld == 0 - and src.get(ATTR_WEATHER_PHENOMENON) is not None - and src.get(ATTR_WEATHER_PHENOMENON) in CONDITION_FOG_CLASSES - ): - cond = ATTR_CONDITION_FOG # Fog - - return cond - - def temperature(self, src=None): - """Return the current temperature.""" - src = src or self._current - temperature = src.get(ATTR_WEATHER_TEMPERATURE) - return float(temperature) if temperature is not None else STATE_UNKNOWN - - def water_temperature(self, src=None): - """Return the current temperature of water.""" - src = src or self._current - temperature = src.get(ATTR_WEATHER_WATER_TEMPERATURE) - return float(temperature) if temperature is not None else STATE_UNKNOWN - - def pressure_mmhg(self, src=None): - """Return the current pressure in mmHg.""" - src = src or self._current - pressure = src.get(ATTR_WEATHER_PRESSURE) - return float(pressure) if pressure is not None else STATE_UNKNOWN - - def pressure_hpa(self, src=None): - """Return the current pressure in hPa.""" - src = src or self._current - pressure = src.get(ATTR_WEATHER_PRESSURE) - return round(pressure * MMHG2HPA, 1) if pressure is not None else STATE_UNKNOWN - - def humidity(self, src=None): - """Return the name of the sensor.""" - src = src or self._current - humidity = src.get(ATTR_WEATHER_HUMIDITY) - return int(humidity) if humidity is not None else STATE_UNKNOWN - - def wind_bearing(self, src=None): - """Return the current wind bearing.""" - src = src or self._current - bearing = int(src.get(ATTR_WEATHER_WIND_BEARING, 0)) - return (bearing - 1) * 45 if bearing > 0 else STATE_UNKNOWN - - def wind_speed_kmh(self, src=None): - """Return the current windspeed in km/h.""" - src = src or self._current - speed = src.get(ATTR_WEATHER_WIND_SPEED) - return round(speed * MS2KMH, 1) if speed is not None else STATE_UNKNOWN - - def wind_speed_ms(self, src=None): - """Return the current windspeed in m/s.""" - src = src or self._current - speed = src.get(ATTR_WEATHER_WIND_SPEED) - return float(speed) if speed is not None else STATE_UNKNOWN - - def precipitation_amount(self, src=None): - """Return the current precipitation amount in mm.""" - src = src or self._current - precipitation = src.get(ATTR_WEATHER_PRECIPITATION_AMOUNT) - return precipitation if precipitation is not None else STATE_UNKNOWN - - def forecast(self, src=None): - """Return the forecast array.""" - src = src or self._forecast - forecast = [] - now = int(time.time()) - dt_util.set_default_time_zone(self._timezone) - for i in src: - fc_time = i.get(ATTR_FORECAST_TIME) - if fc_time is None: - continue - - data = { - ATTR_FORECAST_TIME: dt_util.as_local( - datetime.utcfromtimestamp(fc_time) - ).isoformat(), - ATTR_FORECAST_CONDITION: self.condition(i), - ATTR_FORECAST_TEMP: self.temperature(i), - ATTR_FORECAST_PRESSURE: self.pressure_hpa(i), - ATTR_FORECAST_HUMIDITY: self.humidity(i), - ATTR_FORECAST_WIND_SPEED: self.wind_speed_kmh(i), - ATTR_FORECAST_WIND_BEARING: self.wind_bearing(i), - ATTR_FORECAST_PRECIPITATION: self.precipitation_amount(i), - } - - if ( - self._mode == FORECAST_MODE_DAILY - and i.get(ATTR_FORECAST_TEMP_LOW) is not None - ): - data[ATTR_FORECAST_TEMP_LOW] = i.get(ATTR_FORECAST_TEMP_LOW) - - if fc_time < now: - forecast = [data] - else: - forecast.append(data) - - return forecast - - @staticmethod - def _get_utime(source, tzone): - local_date = source - if len(source) <= 10: - local_date += "T00:00:00" - tz_h, tz_m = divmod(abs(tzone), 60) - local_date += f"+{tz_h:02}:{tz_m:02}" if tzone >= 0 else f"-{tz_h:02}:{tz_m:02}" - return dt_util.as_timestamp(local_date) - - async def async_update(self) -> bool: - """Get the latest data from Gismeteo.""" - if not self._location_key: - await self.async_get_location() - - url = (ENDPOINT_URL + "/forecast/?city={}&lang=en").format(self._location_key) - cache_fname = f"forecast_{self._location_key}" - - response = await self._async_get_data(url, cache_fname) - try: - xml = etree.fromstring(response) - tzone = int(xml.find("location").get("tzone")) - current = xml.find("location/fact") - current_v = current.find("values") - - self._current = { - ATTR_SUNRISE: self._get(current, "sunrise", int), - ATTR_SUNSET: self._get(current, "sunset", int), - ATTR_WEATHER_CONDITION: self._get(current_v, "descr"), - ATTR_WEATHER_TEMPERATURE: self._get(current_v, "tflt", float), - ATTR_WEATHER_PRESSURE: self._get(current_v, "p", int), - ATTR_WEATHER_HUMIDITY: self._get(current_v, "hum", int), - ATTR_WEATHER_WIND_SPEED: self._get(current_v, "ws", int), - ATTR_WEATHER_WIND_BEARING: self._get(current_v, "wd", int), - ATTR_WEATHER_CLOUDINESS: self._get(current_v, "cl", int), - ATTR_WEATHER_PRECIPITATION_TYPE: self._get(current_v, "pt", int), - ATTR_WEATHER_PRECIPITATION_AMOUNT: self._get(current_v, "prflt", float), - ATTR_WEATHER_PRECIPITATION_INTENSITY: self._get(current_v, "pr", int), - ATTR_WEATHER_STORM: (self._get(current_v, "ts") == 1), - ATTR_WEATHER_GEOMAGNETIC_FIELD: self._get(current_v, "grade", int), - ATTR_WEATHER_PHENOMENON: self._get(current_v, "ph", int), - ATTR_WEATHER_WATER_TEMPERATURE: self._get(current_v, "water_t", float), - } - - self._forecast = [] - if self._mode == FORECAST_MODE_HOURLY: - for day in xml.findall("location/day"): - sunrise = self._get(day, "sunrise", int) - sunset = self._get(day, "sunset", int) - - for i in day.findall("forecast"): - fc_v = i.find("values") - data = { - ATTR_SUNRISE: sunrise, - ATTR_SUNSET: sunset, - ATTR_FORECAST_TIME: self._get_utime(i.get("valid"), tzone), - ATTR_FORECAST_CONDITION: self._get(fc_v, "descr"), - ATTR_FORECAST_TEMP: self._get(fc_v, "t", int), - ATTR_FORECAST_PRESSURE: self._get(fc_v, "p", int), - ATTR_FORECAST_HUMIDITY: self._get(fc_v, "hum", int), - ATTR_FORECAST_WIND_SPEED: self._get(fc_v, "ws", int), - ATTR_FORECAST_WIND_BEARING: self._get(fc_v, "wd", int), - ATTR_FORECAST_CLOUDINESS: self._get(fc_v, "cl", int), - ATTR_FORECAST_PRECIPITATION_TYPE: self._get( - fc_v, "pt", int - ), - ATTR_FORECAST_PRECIPITATION_AMOUNT: self._get( - fc_v, "prflt", float - ), - ATTR_FORECAST_PRECIPITATION_INTENSITY: self._get( - fc_v, "pr", int - ), - ATTR_FORECAST_STORM: (fc_v.get("ts") == 1), - ATTR_FORECAST_GEOMAGNETIC_FIELD: self._get( - fc_v, "grade", int - ), - } - self._forecast.append(data) - - else: # self._mode == FORECAST_MODE_DAILY - for day in xml.findall("location/day[@descr]"): - data = { - ATTR_SUNRISE: self._get(day, "sunrise", int), - ATTR_SUNSET: self._get(day, "sunset", int), - ATTR_FORECAST_TIME: self._get_utime(day.get("date"), tzone), - ATTR_FORECAST_CONDITION: self._get(day, "descr"), - ATTR_FORECAST_TEMP: self._get(day, "tmax", int), - ATTR_FORECAST_TEMP_LOW: self._get(day, "tmin", int), - ATTR_FORECAST_PRESSURE: self._get(day, "p", int), - ATTR_FORECAST_HUMIDITY: self._get(day, "hum", int), - ATTR_FORECAST_WIND_SPEED: self._get(day, "ws", int), - ATTR_FORECAST_WIND_BEARING: self._get(day, "wd", int), - ATTR_FORECAST_CLOUDINESS: self._get(day, "cl", int), - ATTR_FORECAST_PRECIPITATION_TYPE: self._get(day, "pt", int), - ATTR_FORECAST_PRECIPITATION_AMOUNT: self._get( - day, "prflt", float - ), - ATTR_FORECAST_PRECIPITATION_INTENSITY: self._get( - day, "pr", int - ), - ATTR_FORECAST_STORM: (self._get(day, "ts") == 1), - ATTR_FORECAST_GEOMAGNETIC_FIELD: self._get( - day, "grademax", int - ), - } - self._forecast.append(data) - - return True - - except (etree.ParseError, TypeError, AttributeError) as ex: - raise ApiError( - "Can't update weather data! Invalid server response." - ) from ex diff --git a/custom_components/gismeteo/manifest.json b/custom_components/gismeteo/manifest.json deleted file mode 100644 index 9d07597..0000000 --- a/custom_components/gismeteo/manifest.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "version": "2.1.0+dev", - "codeowners": [ - "@limych" - ], - "dependencies": [ - "weather" - ], - "documentation": "https://github.com/Limych/ha-gismeteo", - "issue_tracker": "https://github.com/Limych/ha-gismeteo/issues", - "domain": "gismeteo", - "name": "Gismeteo", - "requirements": [], - "config_flow": true -} diff --git a/custom_components/gismeteo/sensor.py b/custom_components/gismeteo/sensor.py index 2b67f15..591db96 100644 --- a/custom_components/gismeteo/sensor.py +++ b/custom_components/gismeteo/sensor.py @@ -1,21 +1,21 @@ -# -# Copyright (c) 2019-2020, Andrey "Limych" Khrolenok +# Copyright (c) 2019-2021, Andrey "Limych" Khrolenok # Creative Commons BY-NC-SA 4.0 International Public License # (see LICENSE.md or https://creativecommons.org/licenses/by-nc-sa/4.0/) -# """ -The Gismeteo Sensor. +The Gismeteo component. For more details about this platform, please refer to the documentation at https://github.com/Limych/ha-gismeteo/ """ + import logging -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, PLATFORM_SCHEMA +import voluptuous as vol +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.components.weather import ATTR_FORECAST_CONDITION from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( - ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, ATTR_ICON, ATTR_NAME, @@ -27,10 +27,8 @@ ) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.update_coordinator import CoordinatorEntity -import voluptuous as vol -from . import ATTRIBUTION, DOMAIN, GismeteoDataUpdateCoordinator +from . import GismeteoDataUpdateCoordinator from .const import ( ATTR_WEATHER_CLOUDINESS, ATTR_WEATHER_GEOMAGNETIC_FIELD, @@ -43,12 +41,12 @@ CONF_YAML, COORDINATOR, DEFAULT_NAME, + DOMAIN, FORECAST_SENSOR_TYPE, - NAME, PRECIPITATION_AMOUNT, SENSOR_TYPES, ) -from .gismeteo import Gismeteo +from .entity import GismeteoEntity _LOGGER = logging.getLogger(__name__) @@ -119,7 +117,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entitie async_add_entities(entities, False) -class GismeteoSensor(CoordinatorEntity): +class GismeteoSensor(GismeteoEntity): """Implementation of an Gismeteo sensor.""" def __init__( @@ -129,35 +127,20 @@ def __init__( coordinator: GismeteoDataUpdateCoordinator, ): """Initialize the sensor.""" - super().__init__(coordinator) - - self._name = name + super().__init__(name, coordinator) self.kind = kind self._state = None self._unit_of_measurement = SENSOR_TYPES[self.kind][ATTR_UNIT_OF_MEASUREMENT] - @property - def _gismeteo(self) -> Gismeteo: - return self.coordinator.gismeteo - - @property - def name(self): - """Return the name of the sensor.""" - return f"{self._name} {SENSOR_TYPES[self.kind][ATTR_NAME]}" - @property def unique_id(self): """Return a unique_id for this entity.""" return f"{self._gismeteo.unique_id}-{self.kind}".lower() @property - def device_info(self): - """Return the device info.""" - return { - "identifiers": {(DOMAIN, self._gismeteo.location_key)}, - "name": NAME, - "entry_type": "service", - } + def name(self): + """Return the name of the sensor.""" + return f"{self._name} {SENSOR_TYPES[self.kind][ATTR_NAME]}" @property def state(self): @@ -234,10 +217,3 @@ def icon(self): def device_class(self): """Return the device_class.""" return SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS] - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return { - ATTR_ATTRIBUTION: ATTRIBUTION, - } diff --git a/custom_components/gismeteo/weather.py b/custom_components/gismeteo/weather.py index 910f1a9..69fcf76 100644 --- a/custom_components/gismeteo/weather.py +++ b/custom_components/gismeteo/weather.py @@ -1,21 +1,18 @@ -# -# Copyright (c) 2019, Andrey "Limych" Khrolenok +# Copyright (c) 2019-2021, Andrey "Limych" Khrolenok # Creative Commons BY-NC-SA 4.0 International Public License # (see LICENSE.md or https://creativecommons.org/licenses/by-nc-sa/4.0/) -# """ -The Gismeteo Weather Provider. +The Gismeteo component. For more details about this platform, please refer to the documentation at https://github.com/Limych/ha-gismeteo/ """ + import logging -from homeassistant.components.weather import ( - DOMAIN as WEATHER_DOMAIN, - PLATFORM_SCHEMA, - WeatherEntity, -) +import voluptuous as vol +from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN +from homeassistant.components.weather import PLATFORM_SCHEMA, WeatherEntity from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONF_API_KEY, @@ -26,21 +23,21 @@ CONF_PLATFORM, TEMP_CELSIUS, ) +from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.update_coordinator import CoordinatorEntity -import voluptuous as vol -from . import ATTRIBUTION, DOMAIN, GismeteoDataUpdateCoordinator +from . import GismeteoDataUpdateCoordinator from .const import ( + ATTRIBUTION, CONF_CACHE_DIR, CONF_YAML, COORDINATOR, DEFAULT_NAME, + DOMAIN, FORECAST_MODE_DAILY, FORECAST_MODE_HOURLY, - NAME, ) -from .gismeteo import Gismeteo +from .entity import GismeteoEntity _LOGGER = logging.getLogger(__name__) @@ -59,7 +56,9 @@ # pylint: disable=unused-argument -async def async_setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, config, add_entities, discovery_info=None +): """Set up the Gismeteo weather platform.""" if CONF_YAML not in hass.data[DOMAIN]: hass.data[DOMAIN].setdefault(CONF_YAML, {}) @@ -74,7 +73,7 @@ async def async_setup_platform(hass, config, add_entities, discovery_info=None): hass.data[DOMAIN][CONF_YAML][uid] = config -async def async_setup_entry(hass, config_entry, async_add_entities): +async def async_setup_entry(hass: HomeAssistant, config_entry, async_add_entities): """Add a Gismeteo weather entities.""" entities = [] if config_entry.source == "import": @@ -98,19 +97,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, False) -class GismeteoWeather(CoordinatorEntity, WeatherEntity): +class GismeteoWeather(GismeteoEntity, WeatherEntity): """Implementation of an Gismeteo sensor.""" def __init__(self, name: str, coordinator: GismeteoDataUpdateCoordinator): """Initialize.""" - super().__init__(coordinator) - - self._name = name + super().__init__(name, coordinator) self._attrs = {} @property - def _gismeteo(self) -> Gismeteo: - return self.coordinator.gismeteo + def unique_id(self): + """Return a unique_id for this entity.""" + return self._gismeteo.unique_id @property def name(self): @@ -122,20 +120,6 @@ def attribution(self): """Return the attribution.""" return ATTRIBUTION - @property - def unique_id(self): - """Return a unique_id for this entity.""" - return self._gismeteo.unique_id - - @property - def device_info(self): - """Return the device info.""" - return { - "identifiers": {(DOMAIN, self._gismeteo.location_key)}, - "name": NAME, - "entry_type": "service", - } - @property def condition(self): """Return the current condition.""" diff --git a/custom_components/integration_blueprint/entity.py b/custom_components/integration_blueprint/entity.py deleted file mode 100644 index 21a3f97..0000000 --- a/custom_components/integration_blueprint/entity.py +++ /dev/null @@ -1,38 +0,0 @@ -"""BlueprintEntity class.""" -from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ID -from homeassistant.helpers.update_coordinator import CoordinatorEntity - -from .const import ATTR_INTEGRATION, ATTRIBUTION, DOMAIN, NAME, VERSION - - -class IntegrationBlueprintEntity(CoordinatorEntity): - """Blueprint entity.""" - - def __init__(self, coordinator, config_entry): - """Class initialization.""" - super().__init__(coordinator) - self.config_entry = config_entry - - @property - def unique_id(self): - """Return a unique ID to use for this entity.""" - return self.config_entry.entry_id - - @property - def device_info(self): - """Return the device info.""" - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "name": NAME, - "model": VERSION, - "manufacturer": NAME, - } - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return { - ATTR_ATTRIBUTION: ATTRIBUTION, - ATTR_ID: str(self.coordinator.data.get("id")), - ATTR_INTEGRATION: DOMAIN, - } diff --git a/tests/__init__.py b/tests/__init__.py index 2439b8b..35cdf3e 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -31,7 +31,7 @@ def get_mock_config_entry(forecast=False) -> MockConfigEntry: ) -async def init_integration(hass, forecast=False) -> MockConfigEntry: +async def init_integration(hass: HomeAssistant, forecast=False) -> MockConfigEntry: """Set up the Gismeteo integration in Home Assistant.""" entry = get_mock_config_entry(forecast) @@ -43,16 +43,16 @@ def mock_data(*args, **kwargs): return location_data if args[0].find("/cities/") >= 0 else forecast_data with patch.object(Gismeteo, "_async_get_data", side_effect=mock_data): - entry.add_to_hass(hass) + entry.add_to_hass(hass: HomeAssistant) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() return entry -async def test_update_interval(hass): +async def test_update_interval(hass: HomeAssistant): """Test correct update interval.""" - entry = await init_integration(hass) + entry = await init_integration(hass: HomeAssistant) assert entry.state == ENTRY_STATE_LOADED @@ -61,7 +61,7 @@ async def test_update_interval(hass): with patch.object(Gismeteo, "async_update") as mock_current: assert mock_current.call_count == 0 - async_fire_time_changed(hass, future) + async_fire_time_changed(hass: HomeAssistant, future) await hass.async_block_till_done() assert mock_current.call_count == 1 diff --git a/tests/test_api.py b/tests/test_api.py index 4695155..e20658d 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -8,11 +8,11 @@ from custom_components.integration_blueprint.api import IntegrationBlueprintApiClient -async def test_api(hass, aioclient_mock, caplog): +async def test_api(hass: HomeAssistant, aioclient_mock, caplog): """Test API calls.""" # To test the api submodule, we first create an instance of our API client - api = IntegrationBlueprintApiClient("test", "test", async_get_clientsession(hass)) + api = IntegrationBlueprintApiClient("test", "test", async_get_clientsession(hass: HomeAssistant)) # Use aioclient_mock which is provided by `pytest_homeassistant_custom_component` # to mock responses to aiohttp requests. In this case we are telling the mock to diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index cf0128b..9ca9084 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -35,7 +35,7 @@ def bypass_setup_fixture(): # Here we simiulate a successful config flow from the backend. # Note that we use the `bypass_get_data` fixture here because # we want the config flow validation to succeed during the test. -async def test_successful_config_flow(hass, bypass_get_data): +async def test_successful_config_flow(hass: HomeAssistant, bypass_get_data): """Test a successful config flow.""" # Initialize a config flow result = await hass.config_entries.flow.async_init( @@ -64,7 +64,7 @@ async def test_successful_config_flow(hass, bypass_get_data): # We use the `error_on_get_data` mock instead of `bypass_get_data` # (note the function parameters) to raise an Exception during # validation of the input config. -async def test_failed_config_flow(hass, error_on_get_data): +async def test_failed_config_flow(hass: HomeAssistant, error_on_get_data): """Test a failed config flow due to credential validation failure.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -82,12 +82,12 @@ async def test_failed_config_flow(hass, error_on_get_data): # Our config flow also has an options flow, so we must test it as well. -async def test_options_flow(hass): +async def test_options_flow(hass: HomeAssistant): """Test an options flow.""" # Create a new MockConfigEntry and add to HASS (we're bypassing config # flow entirely) entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, entry_id="test") - entry.add_to_hass(hass) + entry.add_to_hass(hass: HomeAssistant) # Initialize an options flow result = await hass.config_entries.options.async_init(entry.entry_id) diff --git a/tests/test_gismeteo.py b/tests/test_gismeteo.py index 353c9e7..a680d21 100644 --- a/tests/test_gismeteo.py +++ b/tests/test_gismeteo.py @@ -15,6 +15,7 @@ from aiohttp import ClientSession from asynctest import CoroutineMock from homeassistant.components.weather import ATTR_WEATHER_WIND_SPEED +from homeassistant.const import HTTP_OK from pytest import raises from pytest_homeassistant_custom_component.common import load_fixture @@ -27,7 +28,6 @@ CONDITION_FOG_CLASSES, FORECAST_MODE_DAILY, FORECAST_MODE_HOURLY, - HTTP_OK, ) from custom_components.gismeteo.gismeteo import ( ApiError, diff --git a/tests/test_init.py b/tests/test_init.py index 5716ee8..2b4af3a 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -15,7 +15,7 @@ from . import get_mock_config_entry, init_integration -async def test_async_setup(hass): +async def test_async_setup(hass: HomeAssistant): """Test a successful setup component.""" await async_setup_component( hass, @@ -27,9 +27,9 @@ async def test_async_setup(hass): await hass.async_block_till_done() -async def test_async_setup_entry(hass): +async def test_async_setup_entry(hass: HomeAssistant): """Test a successful setup entry.""" - await init_integration(hass) + await init_integration(hass: HomeAssistant) state = hass.states.get("weather.home") assert state is not None @@ -37,7 +37,7 @@ async def test_async_setup_entry(hass): assert state.state == "snowy" -async def test_config_not_ready(hass): +async def test_config_not_ready(hass: HomeAssistant): """Test for setup failure if connection to Gismeteo is missing.""" entry = get_mock_config_entry() @@ -50,15 +50,15 @@ def mock_data(*args, **kwargs): raise ApiError with patch.object(Gismeteo, "_async_get_data", side_effect=mock_data): - entry.add_to_hass(hass) + entry.add_to_hass(hass: HomeAssistant) await hass.config_entries.async_setup(entry.entry_id) assert entry.state == ENTRY_STATE_SETUP_RETRY -async def test_unload_entry(hass): +async def test_unload_entry(hass: HomeAssistant): """Test successful unload of entry.""" - entry = await init_integration(hass) + entry = await init_integration(hass: HomeAssistant) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.state == ENTRY_STATE_LOADED diff --git a/tests/test_sensor.py b/tests/test_sensor.py index 6bd9f44..41721b7 100644 --- a/tests/test_sensor.py +++ b/tests/test_sensor.py @@ -8,7 +8,7 @@ from custom_components.gismeteo.const import CONF_FORECAST, SENSOR_TYPES -async def test_async_setup_platform(hass): +async def test_async_setup_platform(hass: HomeAssistant): """Test platform setup.""" config = { SENSOR_DOMAIN: [ @@ -21,5 +21,5 @@ async def test_async_setup_platform(hass): ] } with assert_setup_component(2, SENSOR_DOMAIN): - assert await async_setup_component(hass, SENSOR_DOMAIN, config) + assert await async_setup_component(hass: HomeAssistant, SENSOR_DOMAIN, config) await hass.async_block_till_done() diff --git a/tests/test_switch.py b/tests/test_switch.py index a48d58e..43a4396 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -11,11 +11,11 @@ from .const import MOCK_CONFIG -async def test_switch_services(hass): +async def test_switch_services(hass: HomeAssistant): """Test switch services.""" # Create a mock entry so we don't have to go through config flow config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG, entry_id="test") - assert await async_setup_entry(hass, config_entry) + assert await async_setup_entry(hass: HomeAssistant, config_entry) await hass.async_block_till_done() # Functions/objects can be patched directly in test code as well and can be used to test diff --git a/tests/test_weather.py b/tests/test_weather.py index f3a97d6..a3f4706 100644 --- a/tests/test_weather.py +++ b/tests/test_weather.py @@ -7,7 +7,7 @@ from custom_components.gismeteo import DOMAIN -async def test_async_setup_platform(hass): +async def test_async_setup_platform(hass: HomeAssistant): """Test platform setup.""" config = { WEATHER_DOMAIN: [ @@ -16,5 +16,5 @@ async def test_async_setup_platform(hass): ] } with assert_setup_component(2, WEATHER_DOMAIN): - assert await async_setup_component(hass, WEATHER_DOMAIN, config) + assert await async_setup_component(hass: HomeAssistant, WEATHER_DOMAIN, config) await hass.async_block_till_done()