diff --git a/.ruff.toml b/.ruff.toml index 8eae70a..70f5aac 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -25,4 +25,4 @@ keep-runtime-typing = true max-complexity = 25 [lint.per-file-ignores] -"tests/*.py" = ["ANN001", "ANN201", "S101", "S311", "SLF001"] +"tests/*.py" = ["ANN001", "ANN201", "ARG001", "PLR2004", "S101", "S311", "SLF001"] diff --git a/README.md b/README.md index 39c7d13..4e819b2 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,11 @@ I put a lot of work into making this repo and component available and updated to > _(float) (Optional) (Default: coordinates from the Home Assistant configuration)_\ > Longitude coordinate to monitor weather of (required if `latitude` is specified). > +> **show_on_map:**\ +> _(boolean) (Optional)_\ +> Enables showing the location of the weather station on the map.\ +> _Default value: false_ +> > **add_sensors:**\ > _(boolean) (Optional) (Default: false)_\ > Enable this option to add current weather and forecast sensors to the frontend. diff --git a/custom_components/gismeteo/__init__.py b/custom_components/gismeteo/__init__.py index aadd253..c215ddd 100644 --- a/custom_components/gismeteo/__init__.py +++ b/custom_components/gismeteo/__init__.py @@ -1,31 +1,33 @@ # Copyright (c) 2019-2024, 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. +""" +The Gismeteo component. For more details about this platform, please refer to the documentation at https://github.com/Limych/ha-gismeteo/ """ -from functools import cached_property import logging +from functools import cached_property +import homeassistant.helpers.config_validation as cv +import voluptuous as vol from aiohttp import ClientConnectorError from async_timeout import timeout -import voluptuous as vol - from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_API_KEY, + CONF_DOMAIN, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, CONF_SENSORS, + CONF_SHOW_ON_MAP, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.storage import STORAGE_DIR from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import ( @@ -38,7 +40,9 @@ from .const import ( CONF_ADD_SENSORS, CONF_CACHE_DIR, + CONF_CACHE_TIME, CONF_FORECAST_DAYS, + CONF_TIMEZONE, COORDINATOR, DOMAIN, DOMAIN_YAML, @@ -59,6 +63,7 @@ vol.Optional(CONF_API_KEY): cv.string, vol.Optional(CONF_LATITUDE): cv.latitude, vol.Optional(CONF_LONGITUDE): cv.longitude, + vol.Optional(CONF_SHOW_ON_MAP, default=False): cv.boolean, vol.Optional(CONF_SENSORS): cv.deprecated, vol.Optional(CONF_ADD_SENSORS, default=False): cv.boolean, vol.Optional(CONF_FORECAST_DAYS): forecast_days_int, @@ -72,7 +77,7 @@ ) -def deslugify(text: str): +def deslugify(text: str) -> str: """Deslugify string.""" return text.replace("_", " ").capitalize() @@ -107,15 +112,18 @@ def _get_api_client( latitude=config.get(CONF_LATITUDE, hass.config.latitude), longitude=config.get(CONF_LONGITUDE, hass.config.longitude), params={ - "domain": DOMAIN, - "timezone": str(hass.config.time_zone), - "cache_dir": config.get(CONF_CACHE_DIR, hass.config.path(STORAGE_DIR)), - "cache_time": UPDATE_INTERVAL.total_seconds(), + CONF_DOMAIN: DOMAIN, + CONF_TIMEZONE: str(hass.config.time_zone), + CONF_CACHE_DIR: config.get(CONF_CACHE_DIR, hass.config.path(STORAGE_DIR)), + CONF_CACHE_TIME: UPDATE_INTERVAL.total_seconds(), + CONF_SHOW_ON_MAP: config.get(CONF_SHOW_ON_MAP, False), }, ) -async def _async_get_coordinator(hass: HomeAssistant, unique_id, config: dict): +async def _async_get_coordinator( + hass: HomeAssistant, unique_id: str | None, config: dict +) -> DataUpdateCoordinator: """Prepare update coordinator instance.""" gismeteo = _get_api_client(hass, config) await gismeteo.async_update_location() @@ -173,7 +181,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return unload_ok -async def update_listener(hass: HomeAssistant, entry: ConfigEntry): +async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: """Update listener.""" await hass.config_entries.async_reload(entry.entry_id) @@ -183,7 +191,7 @@ class GismeteoDataUpdateCoordinator(DataUpdateCoordinator): def __init__( self, hass: HomeAssistant, unique_id: str | None, gismeteo: GismeteoApiClient - ): + ) -> None: """Initialize.""" super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL) @@ -200,7 +208,7 @@ async def _async_update_data(self) -> _DataT: try: async with timeout(10): await self.gismeteo.async_update() - return self.gismeteo.current_data - except (ApiError, ClientConnectorError) as error: raise UpdateFailed(error) from error + else: + return self.gismeteo.current_data diff --git a/custom_components/gismeteo/api.py b/custom_components/gismeteo/api.py index b7932e3..a5d4198 100644 --- a/custom_components/gismeteo/api.py +++ b/custom_components/gismeteo/api.py @@ -49,7 +49,7 @@ ATTR_FORECAST_WIND_BEARING, Forecast, ) -from homeassistant.const import ATTR_ID, ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.const import ATTR_ID, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_SHOW_ON_MAP from homeassistant.helpers.typing import StateType from homeassistant.util import Throttle from homeassistant.util import dt as dt_util @@ -68,6 +68,8 @@ ATTR_FORECAST_ROAD_CONDITION, ATTR_FORECAST_WATER_TEMPERATURE, ATTR_FORECAST_WIND_BEARING_LABEL, + ATTR_LAT, + ATTR_LON, ATTR_SUNRISE, ATTR_SUNSET, CONDITION_FOG_CLASSES, @@ -139,6 +141,9 @@ def __init__( # noqa: PLR0913 self._session = session self._cache = Cache(params) if params.get("cache_dir") is not None else None + self._latitude = latitude + self._longitude = longitude + self._show_on_map = params.get(CONF_SHOW_ON_MAP, False) self._attributes = {} if location_key is not None: @@ -148,10 +153,7 @@ def __init__( # noqa: PLR0913 } elif self._valid_coordinates(latitude, longitude): _LOGGER.debug("Place coordinates used") - self._attributes = { - ATTR_LATITUDE: latitude, - ATTR_LONGITUDE: longitude, - } + else: raise InvalidCoordinatesError @@ -179,7 +181,15 @@ def _valid_coordinates(latitude: float, longitude: float) -> bool: @property def attributes(self) -> dict[str, Any] | None: """Return an attributes.""" - return self._attributes + attrs = self._attributes.copy() # type: dict[str, Any] + if self._show_on_map: + attrs[ATTR_LATITUDE] = self._latitude + attrs[ATTR_LONGITUDE] = self._longitude + else: + attrs[ATTR_LAT] = self._latitude + attrs[ATTR_LON] = self._longitude + + return attrs @property def current_data(self) -> dict[str, Any]: @@ -245,30 +255,22 @@ async def _async_get_data( async def async_update_location(self) -> None: """Retreive location data from Gismeteo.""" - if ( - self._attributes[ATTR_LATITUDE] == 0 - and self._attributes[ATTR_LONGITUDE] == 0 - ): + if self._latitude == 0 and self._longitude == 0: return url = ( - ENDPOINT_URL + f"/cities/?lat={self._attributes[ATTR_LATITUDE]}" - f"&lng={self._attributes[ATTR_LONGITUDE]}&count=1&lang=en" - ) - cache_fname = ( - f"location_{self._attributes[ATTR_LATITUDE]}" - f"_{self._attributes[ATTR_LONGITUDE]}" + ENDPOINT_URL + f"/cities/?lat={self._latitude}" + f"&lng={self._longitude}&count=1&lang=en" ) + 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._attributes = { - ATTR_ID: self._get(item, "id", int), - ATTR_LATITUDE: self._get(item, "lat", float), - ATTR_LONGITUDE: self._get(item, "lng", float), - } + self._attributes[ATTR_ID] = self._get(item, "id", int) + self._latitude = self._get(item, "lat", float) + self._longitude = self._get(item, "lng", float) except (ETree.ParseError, TypeError, AttributeError) as ex: msg = "Can't retrieve location data! Invalid server response." diff --git a/custom_components/gismeteo/config_flow.py b/custom_components/gismeteo/config_flow.py index f82f6fc..bb77d74 100644 --- a/custom_components/gismeteo/config_flow.py +++ b/custom_components/gismeteo/config_flow.py @@ -1,23 +1,30 @@ # 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. +""" +The Gismeteo component. For more details about this platform, please refer to the documentation at https://github.com/Limych/ha-gismeteo/ """ import logging +from collections.abc import Mapping +from typing import Any +import homeassistant.helpers.config_validation as cv +import voluptuous as vol from aiohttp import ClientConnectorError, ClientError from async_timeout import timeout -import voluptuous as vol - from homeassistant import config_entries -from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, + CONF_SHOW_ON_MAP, +) from homeassistant.core import callback -import homeassistant.helpers.config_validation as cv from . import _get_api_client, forecast_days_int # pylint: disable=unused-import from .api import ApiError @@ -29,6 +36,8 @@ _LOGGER = logging.getLogger(__name__) +type ConfigType = Mapping[str, Any] | None + class GismeteoFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Config flow for Gismeteo.""" @@ -36,12 +45,15 @@ class GismeteoFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL - def __init__(self): + def __init__(self) -> None: """Init config flow.""" self._errors = {} - async def async_step_import(self, platform_config): - """Import a config entry. + async def async_step_import( + self, platform_config: ConfigType + ) -> config_entries.ConfigFlowResult: + """ + 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. @@ -51,10 +63,12 @@ async def async_step_import(self, platform_config): return self.async_create_entry(title="configuration.yaml", data=platform_config) - async def async_step_user(self, user_input=None): + async def async_step_user( + self, user_input: ConfigType = None + ) -> config_entries.ConfigFlowResult: """Handle a flow initialized by the user.""" for entry in self._async_current_entries(): - if entry.source == SOURCE_IMPORT: + if entry.source == config_entries.SOURCE_IMPORT: return self.async_abort(reason="no_mixed_config") self._errors = {} @@ -73,7 +87,7 @@ async def async_step_user(self, user_input=None): return self._show_config_form(user_input) - def _show_config_form(self, config): + def _show_config_form(self, config: ConfigType) -> config_entries.ConfigFlowResult: if config is None: config = {} return self.async_show_form( @@ -99,7 +113,7 @@ def _show_config_form(self, config): @staticmethod @callback - def async_get_options_flow(config_entry): + def async_get_options_flow(config_entry: ConfigEntry) -> config_entries.OptionsFlow: """Get component options flow.""" return GismeteoOptionsFlowHandler(config_entry) @@ -107,19 +121,23 @@ def async_get_options_flow(config_entry): class GismeteoOptionsFlowHandler(config_entries.OptionsFlow): """Gismeteo config flow options handler.""" - def __init__(self, config_entry): + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize HACS options flow.""" self.config_entry = config_entry self.options = dict(config_entry.options) - async def async_step_init(self, user_input=None): # pylint: disable=unused-argument + async def async_step_init( + self, user_input: ConfigType = None # noqa: ARG002 + ) -> config_entries.ConfigFlowResult: # pylint: disable=unused-argument """Manage the options.""" if self.config_entry.source == config_entries.SOURCE_IMPORT: return self.async_abort(reason="no_options_available") return await self.async_step_user() - async def async_step_user(self, user_input: dict | None = None): + async def async_step_user( + self, user_input: ConfigType = None + ) -> config_entries.ConfigFlowResult: """Handle a flow initialized by the user.""" if user_input is not None: if CONF_FORECAST_DAYS in self.options: @@ -132,6 +150,10 @@ async def async_step_user(self, user_input: dict | None = None): data_schema=self.add_suggested_values_to_schema( vol.Schema( { + vol.Required( + CONF_SHOW_ON_MAP, + default=self.options.get(CONF_SHOW_ON_MAP, False), + ): bool, vol.Required(CONF_ADD_SENSORS, default=False): bool, vol.Optional(CONF_FORECAST_DAYS): forecast_days_int, } @@ -140,7 +162,7 @@ async def async_step_user(self, user_input: dict | None = None): ), ) - async def _update_options(self): + async def _update_options(self) -> config_entries.ConfigFlowResult: """Update config entry options.""" return self.async_create_entry( title=self.config_entry.data.get(CONF_NAME), data=self.options diff --git a/custom_components/gismeteo/const.py b/custom_components/gismeteo/const.py index b2373f4..86b64ce 100644 --- a/custom_components/gismeteo/const.py +++ b/custom_components/gismeteo/const.py @@ -1,7 +1,8 @@ # Copyright (c) 2019-2024, 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. +""" +The Gismeteo component. For more details about this platform, please refer to the documentation at https://github.com/Limych/ha-gismeteo/ @@ -61,7 +62,9 @@ PLATFORMS: Final = [Platform.SENSOR, Platform.WEATHER] # Configuration and options +CONF_TIMEZONE: Final = "timezone" CONF_CACHE_DIR: Final = "cache_dir" +CONF_CACHE_TIME: Final = "cache_time" CONF_ADD_SENSORS: Final = "add_sensors" CONF_FORECAST_DAYS: Final = "forecast_days" @@ -87,6 +90,9 @@ ATTR_FORECAST_ROAD_CONDITION: Final = "road_condition" ATTR_FORECAST_RAIN_AMOUNT: Final = "rain_amount" ATTR_FORECAST_SNOW_AMOUNT: Final = "snow_amount" +# +ATTR_LAT = "lat" +ATTR_LON = "lon" COORDINATOR: Final = "coordinator" UNDO_UPDATE_LISTENER: Final = "undo_update_listener" @@ -95,7 +101,8 @@ # PARSER_URL_FORMAT: Final = "https://www.gismeteo.ru/weather-{}/10-days/" PARSER_USER_AGENT: Final = ( - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.182 Safari/537.36 Edg/88.0.705.81" + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)" + " Chrome/128.0.0.0 Safari/537.36" ) UPDATE_INTERVAL: Final = timedelta(minutes=5) diff --git a/custom_components/gismeteo/translations/en.json b/custom_components/gismeteo/translations/en.json index 14dc2c5..70296ed 100644 --- a/custom_components/gismeteo/translations/en.json +++ b/custom_components/gismeteo/translations/en.json @@ -227,6 +227,7 @@ "step": { "user": { "data": { + "show_on_map": "Show entities on map", "add_sensors": "Sensor entities enabled", "forecast_days": "Forecast sensors to show (days)" }, diff --git a/custom_components/gismeteo/translations/ru.json b/custom_components/gismeteo/translations/ru.json index 06dccd7..221b23f 100644 --- a/custom_components/gismeteo/translations/ru.json +++ b/custom_components/gismeteo/translations/ru.json @@ -227,6 +227,7 @@ "step": { "user": { "data": { + "show_on_map": "Показывать объекты на карте", "add_sensors": "Сенсоры включены", "forecast_days": "Сенсоры прогноза на (дней)" }, diff --git a/tests/const.py b/tests/const.py index 3317d7c..575a497 100644 --- a/tests/const.py +++ b/tests/const.py @@ -1,7 +1,13 @@ """Constants for tests.""" +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_NAME, + CONF_SHOW_ON_MAP, +) + from custom_components.gismeteo.const import CONF_ADD_SENSORS -from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME TEST_NAME = "Home" TEST_UNIQUE_ID = "test_id" @@ -24,5 +30,6 @@ CONF_LATITUDE: TEST_LATITUDE, CONF_LONGITUDE: TEST_LONGITUDE, CONF_ADD_SENSORS: True, + CONF_SHOW_ON_MAP: False, }, } diff --git a/tests/test_api.py b/tests/test_api.py index f4600ed..d200037 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -5,25 +5,8 @@ from typing import Any from unittest.mock import AsyncMock, patch -from aiohttp import ClientSession import pytest -from pytest import raises -from pytest_homeassistant_custom_component.common import load_fixture - -from custom_components.gismeteo.api import ( - ApiError, - GismeteoApiClient, - InvalidCoordinatesError, -) -from custom_components.gismeteo.const import ( - ATTR_FORECAST_IS_STORM, - ATTR_FORECAST_PHENOMENON, - ATTR_FORECAST_PRECIPITATION_INTENSITY, - ATTR_FORECAST_PRECIPITATION_TYPE, - ATTR_SUNRISE, - CONDITION_FOG_CLASSES, - ForecastMode, -) +from aiohttp import ClientSession from homeassistant.components.weather import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLOUDY, @@ -44,6 +27,24 @@ ) from homeassistant.const import ATTR_ID from homeassistant.util import dt as dt_util +from pytest_homeassistant_custom_component.common import load_fixture + +from custom_components.gismeteo.api import ( + ApiError, + GismeteoApiClient, + InvalidCoordinatesError, +) +from custom_components.gismeteo.const import ( + ATTR_FORECAST_IS_STORM, + ATTR_FORECAST_PHENOMENON, + ATTR_FORECAST_PRECIPITATION_INTENSITY, + ATTR_FORECAST_PRECIPITATION_TYPE, + ATTR_LAT, + ATTR_LON, + ATTR_SUNRISE, + CONDITION_FOG_CLASSES, + ForecastMode, +) LATITUDE = 52.0677904 LONGITUDE = 19.4795644 @@ -54,7 +55,7 @@ @pytest.fixture(autouse=True) -def patch_time(): +def _patch_time() -> None: """Patch time functions.""" with ( patch.object(dt_util, "now", return_value=MOCK_NOW), @@ -82,7 +83,7 @@ async def test__valid_coordinates(): assert GismeteoApiClient._valid_coordinates(lat, lon) is False async with ClientSession() as client: - with raises(InvalidCoordinatesError): + with pytest.raises(InvalidCoordinatesError): GismeteoApiClient(client, latitude=lat_invalid[0], longitude=lon_invalid[0]) @@ -114,7 +115,7 @@ async def test__async_get_data(mock_get): # async with ClientSession() as client: gismeteo = GismeteoApiClient(client, latitude=LATITUDE, longitude=LONGITUDE) - with raises(ApiError): + with pytest.raises(ApiError): await gismeteo._async_get_data("some_url") @@ -155,7 +156,7 @@ async def test_async_update_location(): ): async with ClientSession() as client: gismeteo = GismeteoApiClient(client, latitude=LATITUDE, longitude=LONGITUDE) - with raises(ApiError): + with pytest.raises(ApiError): await gismeteo.async_update_location() with patch.object( @@ -165,7 +166,7 @@ async def test_async_update_location(): ): async with ClientSession() as client: gismeteo = GismeteoApiClient(client, latitude=LATITUDE, longitude=LONGITUDE) - with raises(ApiError): + with pytest.raises(ApiError): await gismeteo.async_update_location() @@ -184,7 +185,7 @@ def test__get_utime(): 2021, 2, 21, tzinfo=dt_util.UTC ) - with raises(ValueError): + with pytest.raises(ValueError): # noqa: PT011 GismeteoApiClient._get_utime("2021-02-", 0) @@ -226,150 +227,150 @@ async def test_async_get_parsed(gismeteo_api): datetime(2021, 2, 21, tzinfo=TZ180): { "geomagnetic": "7", "humidity": "61", - "icon-snow": "–", + "icon-snow": "–", # noqa: RUF001 "icon-tooltip": None, "pollen-birch": "2", - "pollen-grass": "–", - "pollen-ragweed": "–", + "pollen-grass": "–", # noqa: RUF001 + "pollen-ragweed": "–", # noqa: RUF001 "precipitation-bars": "0,3", "radiation": "2", "roadcondition": "Сухая дорога", - "wind-direction": "С", - "wind-gust": "–", + "wind-direction": "С", # noqa: RUF001 + "wind-gust": "–", # noqa: RUF001 "wind-speed": "2", }, datetime(2021, 2, 22, tzinfo=TZ180): { "geomagnetic": "6", "humidity": "51", - "icon-snow": "–", + "icon-snow": "–", # noqa: RUF001 "icon-tooltip": None, "pollen-birch": "2", - "pollen-grass": "–", - "pollen-ragweed": "–", + "pollen-grass": "–", # noqa: RUF001 + "pollen-ragweed": "–", # noqa: RUF001 "precipitation-bars": "0", "radiation": "4", "roadcondition": "Сухая дорога", - "wind-direction": "С", + "wind-direction": "С", # noqa: RUF001 "wind-gust": "4", "wind-speed": "1", }, datetime(2021, 2, 23, tzinfo=TZ180): { "geomagnetic": "4", "humidity": "51", - "icon-snow": "–", + "icon-snow": "–", # noqa: RUF001 "icon-tooltip": None, "pollen-birch": "2", - "pollen-grass": "–", - "pollen-ragweed": "–", + "pollen-grass": "–", # noqa: RUF001 + "pollen-ragweed": "–", # noqa: RUF001 "precipitation-bars": "0", "radiation": "3", "roadcondition": "Сухая дорога", - "wind-direction": "СЗ", + "wind-direction": "СЗ", # noqa: RUF001 "wind-gust": "4", "wind-speed": "1", }, datetime(2021, 2, 24, tzinfo=TZ180): { "geomagnetic": "2", "humidity": "48", - "icon-snow": "–", + "icon-snow": "–", # noqa: RUF001 "icon-tooltip": None, "pollen-birch": "1", - "pollen-grass": "–", - "pollen-ragweed": "–", + "pollen-grass": "–", # noqa: RUF001 + "pollen-ragweed": "–", # noqa: RUF001 "precipitation-bars": "0", "radiation": "5", "roadcondition": "Сухая дорога", - "wind-direction": "СЗ", + "wind-direction": "СЗ", # noqa: RUF001 "wind-gust": "4", "wind-speed": "1", }, datetime(2021, 2, 25, tzinfo=TZ180): { "geomagnetic": "2", "humidity": "48", - "icon-snow": "–", + "icon-snow": "–", # noqa: RUF001 "icon-tooltip": None, "pollen-birch": "1", - "pollen-grass": "–", - "pollen-ragweed": "–", + "pollen-grass": "–", # noqa: RUF001 + "pollen-ragweed": "–", # noqa: RUF001 "precipitation-bars": "0", "radiation": "7", "roadcondition": "Нет данных", - "wind-direction": "СЗ", + "wind-direction": "СЗ", # noqa: RUF001 "wind-gust": "4", "wind-speed": "1", }, datetime(2021, 2, 26, tzinfo=TZ180): { "geomagnetic": "2", "humidity": "65", - "icon-snow": "–", + "icon-snow": "–", # noqa: RUF001 "icon-tooltip": None, "pollen-birch": "3", - "pollen-grass": "–", - "pollen-ragweed": "–", + "pollen-grass": "–", # noqa: RUF001 + "pollen-ragweed": "–", # noqa: RUF001 "precipitation-bars": "5,9", "radiation": "1", "roadcondition": "Нет данных", - "wind-direction": "С", + "wind-direction": "С", # noqa: RUF001 "wind-gust": "4", "wind-speed": "1", }, datetime(2021, 2, 27, tzinfo=TZ180): { "geomagnetic": "2", "humidity": "66", - "icon-snow": "–", + "icon-snow": "–", # noqa: RUF001 "icon-tooltip": None, "pollen-birch": "2", - "pollen-grass": "–", - "pollen-ragweed": "–", + "pollen-grass": "–", # noqa: RUF001 + "pollen-ragweed": "–", # noqa: RUF001 "precipitation-bars": "0", "radiation": "7", "roadcondition": "Нет данных", - "wind-direction": "С", + "wind-direction": "С", # noqa: RUF001 "wind-gust": "6", "wind-speed": "2", }, datetime(2021, 2, 28, tzinfo=TZ180): { "geomagnetic": "2", "humidity": "56", - "icon-snow": "–", + "icon-snow": "–", # noqa: RUF001 "icon-tooltip": None, "pollen-birch": "0", - "pollen-grass": "–", - "pollen-ragweed": "–", + "pollen-grass": "–", # noqa: RUF001 + "pollen-ragweed": "–", # noqa: RUF001 "precipitation-bars": "0", "radiation": "7", "roadcondition": "Нет данных", - "wind-direction": "СЗ", + "wind-direction": "СЗ", # noqa: RUF001 "wind-gust": "3", "wind-speed": "1", }, datetime(2021, 3, 1, tzinfo=TZ180): { "geomagnetic": "2", "humidity": "55", - "icon-snow": "–", + "icon-snow": "–", # noqa: RUF001 "icon-tooltip": None, - "pollen-birch": "–", - "pollen-grass": "–", - "pollen-ragweed": "–", + "pollen-birch": "–", # noqa: RUF001 + "pollen-grass": "–", # noqa: RUF001 + "pollen-ragweed": "–", # noqa: RUF001 "precipitation-bars": "0", "radiation": "6", "roadcondition": "Нет данных", - "wind-direction": "СЗ", + "wind-direction": "СЗ", # noqa: RUF001 "wind-gust": "4", "wind-speed": "2", }, datetime(2021, 3, 2, tzinfo=TZ180): { "geomagnetic": "2", "humidity": "54", - "icon-snow": "–", + "icon-snow": "–", # noqa: RUF001 "icon-tooltip": None, - "pollen-birch": "–", - "pollen-grass": "–", - "pollen-ragweed": "–", + "pollen-birch": "–", # noqa: RUF001 + "pollen-grass": "–", # noqa: RUF001 + "pollen-ragweed": "–", # noqa: RUF001 "precipitation-bars": "0", "radiation": "6", "roadcondition": "Нет данных", - "wind-direction": "З", + "wind-direction": "З", # noqa: RUF001 "wind-gust": "5", "wind-speed": "2", }, @@ -380,14 +381,14 @@ async def test_async_get_parsed(gismeteo_api): async def init_gismeteo( location_key: int | None = LOCATION_KEY, - data: Any = False, + data: Any = False, # noqa: FBT002 ): """Prepare Gismeteo object.""" forecast_data = data if data is not False else load_fixture("forecast.xml") forecast_parsed_data = load_fixture("forecast_parsed.html") # pylint: disable=unused-argument - def mock_data(*args, **kwargs): + def mock_data(*args, **kwargs) -> str: # noqa: ANN002, ANN003 return ( forecast_data if args[0].find("/forecast/") >= 0 else forecast_parsed_data ) @@ -466,7 +467,11 @@ async def test_api_init(): "road_condition": "Сухая дорога", } - assert gismeteo.attributes == {"id": LOCATION_KEY} + assert gismeteo.attributes == { + ATTR_ID: LOCATION_KEY, + ATTR_LAT: LATITUDE, + ATTR_LON: LONGITUDE, + } assert gismeteo.current_data == expected_current assert gismeteo.forecast_data(0) == expected_forecast @@ -479,15 +484,15 @@ async def test_async_update(): assert gismeteo.current_data[ATTR_FORECAST_HUMIDITY] == 86 assert gismeteo.current_data[ATTR_FORECAST_PHENOMENON] == 71 - with raises(ApiError): + with pytest.raises(ApiError): await init_gismeteo(location_key=None) - with raises(ApiError): + with pytest.raises(ApiError): await init_gismeteo(data=None) - with raises(ApiError): + with pytest.raises(ApiError): await init_gismeteo(data="qwe") -async def test_condition(): +async def test_condition(): # noqa: PLR0915 """Test condition.""" gismeteo = await init_gismeteo() @@ -831,38 +836,3 @@ async def test_road_condition(): gismeteo_d.road_condition(gismeteo_d.forecast_data(day, ForecastMode.DAILY)) == exp ) - - -# -# async def test_forecast(): -# """Test forecast.""" -# with patch( -# "homeassistant.util.dt.now", -# return_value=datetime(2021, 2, 26, tzinfo=dt_util.UTC), -# ): -# gismeteo_d = await init_gismeteo() -# -# assert gismeteo_d.forecast(ForecastMode.DAILY) == [ -# { -# "datetime": datetime(2021, 2, 26, tzinfo=TZ180), -# "condition": "rainy", -# "temperature": 4.0, -# "pressure": 0.0, -# "humidity": 89, -# "wind_speed": 7, -# "wind_bearing": 270, -# "precipitation": 0.3, -# "templow": 2, -# }, -# { -# "datetime": datetime(2021, 2, 27, tzinfo=TZ180), -# "condition": "cloudy", -# "temperature": 2.0, -# "pressure": 0.0, -# "humidity": 87, -# "wind_speed": 6, -# "wind_bearing": 270, -# "precipitation": 0.0, -# "templow": 0, -# }, -# ] diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index fbce0b4..51b668e 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -4,12 +4,12 @@ from unittest.mock import patch import pytest +from homeassistant import config_entries, data_entry_flow +from homeassistant.const import CONF_NAME, CONF_SHOW_ON_MAP +from homeassistant.core import HomeAssistant from pytest_homeassistant_custom_component.common import MockConfigEntry from custom_components.gismeteo.const import CONF_ADD_SENSORS, DOMAIN -from homeassistant import config_entries, data_entry_flow -from homeassistant.const import CONF_NAME -from homeassistant.core import HomeAssistant from .const import TEST_CONFIG @@ -18,7 +18,7 @@ # since we only want to test the config flow. We test the # actual functionality of the integration in other test modules. @pytest.fixture(autouse=True) -def bypass_setup_fixture(): +def _bypass_setup_fixture() -> None: """Prevent setup.""" with ( patch( @@ -110,6 +110,7 @@ async def test_options_flow(hass: HomeAssistant): # Verify that the options were updated assert entry.options == { CONF_ADD_SENSORS: False, + CONF_SHOW_ON_MAP: False, }