commit e436fafd9ef4abd4f3e20c58cb63ff52b3ad36f2 Author: devilr33f <86633411+devilr33f@users.noreply.github.com> Date: Tue Nov 4 11:21:01 2025 +0600 feat: initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a944e4a --- /dev/null +++ b/.gitignore @@ -0,0 +1,210 @@ +# Created by https://www.toptal.com/developers/gitignore/api/python,venv,visualstudiocode +# Edit at https://www.toptal.com/developers/gitignore?templates=python,venv,visualstudiocode + +scripts + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +### venv ### +# Virtualenv +# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ +[Bb]in +[Ii]nclude +[Ll]ib +[Ll]ib64 +[Ll]ocal +[Ss]cripts +pyvenv.cfg +pip-selfcheck.json + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# End of https://www.toptal.com/developers/gitignore/api/python,venv,visualstudiocode + diff --git a/README.md b/README.md new file mode 100644 index 0000000..6e3cc42 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# 2GIS Weather – Home Assistant Custom Integration + +Custom integration to fetch weather from 2GIS and expose a Weather entity with hourly/daily forecasts and extra sensors. + +## Install (local dev) + +- Copy `custom_components/two_gis_weather` into your Home Assistant `config/custom_components/` directory. +- Restart Home Assistant. + +## Configure + +- In Home Assistant: Settings → Devices & Services → Add Integration → 2GIS Weather +- Provide your API key and coordinates (defaults to your HA location). + + diff --git a/custom_components/two_gis_weather/__init__.py b/custom_components/two_gis_weather/__init__.py new file mode 100644 index 0000000..d69c3ae --- /dev/null +++ b/custom_components/two_gis_weather/__init__.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN, PLATFORMS +from .coordinator import TwoGisWeatherUpdateCoordinator + + +async def async_setup(hass: HomeAssistant, _config: dict) -> bool: + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + coordinator = TwoGisWeatherUpdateCoordinator(hass, entry) + await coordinator.async_config_entry_first_refresh() + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data.get(DOMAIN, {}).pop(entry.entry_id, None) + return unload_ok + + diff --git a/custom_components/two_gis_weather/api.py b/custom_components/two_gis_weather/api.py new file mode 100644 index 0000000..c15b823 --- /dev/null +++ b/custom_components/two_gis_weather/api.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import uuid +from typing import Any + +import async_timeout +from aiohttp import ClientResponse +from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import API_BASE, DEFAULT_USER_AGENT, REQUEST_TIMEOUT + + +class TwoGisWeatherClient: + def __init__( + self, + hass: HomeAssistant, + api_key: str, + latitude: float, + longitude: float, + language: str = "en-US", + user_agent: str = DEFAULT_USER_AGENT, + ) -> None: + self.hass = hass + self.api_key = api_key + self.latitude = latitude + self.longitude = longitude + self.language = language + self.user_agent = user_agent + self._last_modified: str | None = None + + def _headers(self) -> dict[str, str]: + headers: dict[str, str] = { + "User-Agent": self.user_agent, + "x-request-id": uuid.uuid4().hex, + "Accept-Language": self.language, + "Accept": "application/json", + } + if self._last_modified: + headers["If-Modified-Since"] = self._last_modified + return headers + + async def async_get_current(self) -> dict[str, Any] | None: + session = async_get_clientsession(self.hass) + url = f"{API_BASE}/forecast" + params = { + "lon": str(self.longitude), + "lat": str(self.latitude), + "key": self.api_key, + } + async with async_timeout.timeout(REQUEST_TIMEOUT): + resp: ClientResponse + async with session.get(url, params=params, headers=self._headers()) as resp: + if resp.status == 304: + return None + resp.raise_for_status() + # Track Last-Modified for conditional requests + last_mod = resp.headers.get("Last-Modified") + if last_mod: + self._last_modified = last_mod + return await resp.json(content_type=None) + + @property + def last_modified(self) -> str | None: + return self._last_modified + + diff --git a/custom_components/two_gis_weather/config_flow.py b/custom_components/two_gis_weather/config_flow.py new file mode 100644 index 0000000..104450d --- /dev/null +++ b/custom_components/two_gis_weather/config_flow.py @@ -0,0 +1,104 @@ +from __future__ import annotations + +from typing import Any +import asyncio +from aiohttp import ClientError, ClientResponseError + +import voluptuous as vol +from homeassistant import config_entries +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult + +from .const import ( + DOMAIN, + CONF_API_KEY, + CONF_NAME, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_LANGUAGE, + CONF_UPDATE_INTERVAL, +) + + +def _default_lat_lon(hass: HomeAssistant) -> tuple[float | None, float | None]: + return hass.config.latitude, hass.config.longitude + + +from .api import TwoGisWeatherClient + + +class TwoGisWeatherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + VERSION = 1 + + async def async_step_user(self, user_input: dict[str, Any] | None = None) -> FlowResult: + errors: dict[str, str] = {} + if user_input is not None: + api_key = user_input[CONF_API_KEY] + name = user_input.get(CONF_NAME) or "2GIS Weather" + lat = float(user_input.get(CONF_LATITUDE) or self.hass.config.latitude) + lon = float(user_input.get(CONF_LONGITUDE) or self.hass.config.longitude) + lang = user_input.get(CONF_LANGUAGE) or "en-US" + + # Prevent duplicates for the same coordinates + await self.async_set_unique_id(f"{lat:.4f},{lon:.4f}") + self._abort_if_unique_id_configured() + + client = TwoGisWeatherClient(self.hass, api_key, lat, lon, lang) + try: + await client.async_get_current() + except ClientResponseError as e: # HTTP error from server + errors["base"] = "invalid_auth" if e.status in (401, 403) else "cannot_connect" + except (asyncio.TimeoutError, ClientError): + errors["base"] = "cannot_connect" + except Exception: # noqa: BLE001 + errors["base"] = "unknown" + else: + return self.async_create_entry( + title=name, + data={ + CONF_API_KEY: api_key, + CONF_NAME: name, + CONF_LATITUDE: lat, + CONF_LONGITUDE: lon, + CONF_LANGUAGE: lang, + }, + ) + + lat, lon = _default_lat_lon(self.hass) + schema = vol.Schema( + { + vol.Required(CONF_API_KEY): str, + vol.Optional(CONF_NAME, default="2GIS Weather"): str, + vol.Optional(CONF_LATITUDE, default=lat): float, + vol.Optional(CONF_LONGITUDE, default=lon): float, + vol.Optional(CONF_LANGUAGE, default="en-US"): str, + } + ) + return self.async_show_form(step_id="user", data_schema=schema) + + async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: + return await self.async_step_user(import_config) + + +class TwoGisWeatherOptionsFlowHandler(config_entries.OptionsFlow): + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + self.config_entry = config_entry + + async def async_step_init(self, user_input: dict[str, Any] | None = None) -> FlowResult: + if user_input is not None: + return self.async_create_entry(title="Options", data=user_input) + + current = {**self.config_entry.options} + schema = vol.Schema( + { + vol.Optional(CONF_UPDATE_INTERVAL, default=current.get(CONF_UPDATE_INTERVAL, 10)): int, + vol.Optional(CONF_LANGUAGE, default=current.get(CONF_LANGUAGE, "en-US")): str, + } + ) + return self.async_show_form(step_id="init", data_schema=schema) + + +async def async_get_options_flow(config_entry: config_entries.ConfigEntry) -> TwoGisWeatherOptionsFlowHandler: # type: ignore[override] + return TwoGisWeatherOptionsFlowHandler(config_entry) + + diff --git a/custom_components/two_gis_weather/const.py b/custom_components/two_gis_weather/const.py new file mode 100644 index 0000000..78e715d --- /dev/null +++ b/custom_components/two_gis_weather/const.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from datetime import timedelta +from homeassistant.const import Platform + + +DOMAIN = "two_gis_weather" +PLATFORMS: list[Platform] = [Platform.WEATHER, Platform.SENSOR] + +API_BASE = "https://mobile-weather.api.2gis.ru/api/1.0" +DEFAULT_USER_AGENT = "2gis/4.0 v4-iphone/7.13.1 (iPhone17,1; iOS 26.0.1; V2:37d4101b-dca7-42a2-ae41-fdc69d122794) application" +REQUEST_TIMEOUT = 10 + +DEFAULT_UPDATE_INTERVAL = timedelta(minutes=10) + +CONF_API_KEY = "api_key" +CONF_NAME = "name" +CONF_LATITUDE = "latitude" +CONF_LONGITUDE = "longitude" +CONF_LANGUAGE = "language" +CONF_UPDATE_INTERVAL = "update_interval" + + diff --git a/custom_components/two_gis_weather/coordinator.py b/custom_components/two_gis_weather/coordinator.py new file mode 100644 index 0000000..84ad905 --- /dev/null +++ b/custom_components/two_gis_weather/coordinator.py @@ -0,0 +1,130 @@ +from __future__ import annotations + +import logging +from typing import Any + +from datetime import datetime, timedelta, timezone +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DEFAULT_UPDATE_INTERVAL, DOMAIN, CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_LANGUAGE +from .api import TwoGisWeatherClient + + +_LOGGER = logging.getLogger(__name__) + + +class TwoGisWeatherUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): + def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: + self.entry = entry + api_key: str = entry.data[CONF_API_KEY] + latitude: float = float(entry.data.get(CONF_LATITUDE, hass.config.latitude)) + longitude: float = float(entry.data.get(CONF_LONGITUDE, hass.config.longitude)) + language: str = entry.data.get(CONF_LANGUAGE, "en-US") + self.client = TwoGisWeatherClient(hass, api_key, latitude, longitude, language) + + super().__init__( + hass, + _LOGGER, + name=f"2GIS Weather ({latitude},{longitude})", + update_interval=DEFAULT_UPDATE_INTERVAL, + ) + + async def _async_update_data(self) -> dict[str, Any]: + try: + raw = await self.client.async_get_current() + except Exception as err: # noqa: BLE001 - narrowed at integration time + raise UpdateFailed(str(err)) from err + + if not raw: + # Not modified: keep previous data + return self.data or {} + + return _normalize_payload(raw) + + +def _mmhg_to_hpa(value: float | int | None) -> float | None: + if value is None: + return None + return round(float(value) * 1.33322, 1) + + +def _tzinfo_from_offset(offset: str | None) -> timezone: + if not offset: + return timezone.utc + sign = 1 if offset.startswith("+") else -1 + try: + hours_str, mins_str = offset[1:].split(":", 1) + hours = int(hours_str) + minutes = int(mins_str) + except Exception: # noqa: BLE001 + return timezone.utc + return timezone(sign * timedelta(hours=hours, minutes=minutes)) + + +def _normalize_payload(data: dict[str, Any]) -> dict[str, Any]: + from .icon_map import to_ha_condition # local import to avoid circulars + + tzinfo = _tzinfo_from_offset(data.get("tz_offset")) + cur = data.get("current", {}) + temp = (cur.get("temperature") or {}) + wind = (cur.get("wind") or {}) + pressure = (cur.get("pressure") or {}) + icon = cur.get("icon") + time_of_day = cur.get("time_of_day") + + current: dict[str, Any] = { + "temperature": temp.get("air"), + "feels_like": temp.get("feels"), + "wind_speed": wind.get("speed"), + "wind_bearing": (wind.get("direction") or "").upper() or None, + "pressure": _mmhg_to_hpa(pressure.get("value")), + "description": cur.get("weather_description"), + "icon": icon, + "condition": to_ha_condition(icon, time_of_day), + } + + hourly: list[dict[str, Any]] = [] + for item in data.get("forecast_hourly", []) or []: + ts = item.get("date") + icon_h = item.get("icon") + temp_h = (item.get("temperature") or {}).get("air") + if ts is None: + continue + dt = datetime.fromtimestamp(int(ts), tz=tzinfo) + hourly.append( + { + "datetime": dt, + "temperature": temp_h, + "condition": to_ha_condition(icon_h, time_of_day), + } + ) + + daily: list[dict[str, Any]] = [] + for item in data.get("forecast_daily", []) or []: + ts = item.get("date") + temps = item.get("temperature") or {} + tmin = (temps.get("min") or {}).get("air") + tmax = (temps.get("max") or {}).get("air") + icon_d = item.get("icon") + if ts is None: + continue + dt = datetime.fromtimestamp(int(ts), tz=tzinfo) + daily.append( + { + "datetime": dt, + "temperature": tmax, + "templow": tmin, + "condition": to_ha_condition(icon_d, time_of_day), + } + ) + + return { + "current": current, + "hourly": hourly, + "daily": daily, + "tz_offset": data.get("tz_offset"), + } + + diff --git a/custom_components/two_gis_weather/icon_map.py b/custom_components/two_gis_weather/icon_map.py new file mode 100644 index 0000000..8bfa320 --- /dev/null +++ b/custom_components/two_gis_weather/icon_map.py @@ -0,0 +1,16 @@ +from __future__ import annotations + + +def to_ha_condition(icon: str, time_of_day: str | None) -> str: + if icon == "d": + return "sunny" if time_of_day == "day" else "clear-night" + mapping = { + "c": "cloudy", + "c_s": "snowy", + "c_r": "rainy", + "c_rs": "snowy-rainy", + "d_c": "partlycloudy", + } + return mapping.get(icon, "cloudy") + + diff --git a/custom_components/two_gis_weather/manifest.json b/custom_components/two_gis_weather/manifest.json new file mode 100644 index 0000000..fd5f50a --- /dev/null +++ b/custom_components/two_gis_weather/manifest.json @@ -0,0 +1,14 @@ +{ + "domain": "two_gis_weather", + "name": "2GIS Weather", + "version": "0.1.0", + "config_flow": true, + "documentation": "https://github.com/devilr33f/ha-2gis-weather", + "issue_tracker": "https://github.com/devilr33f/ha-2gis-weather/issues", + "iot_class": "cloud_polling", + "integration_type": "hub", + "requirements": [], + "codeowners": ["@devilr33f"], + "loggers": ["custom_components.two_gis_weather"] +} + diff --git a/custom_components/two_gis_weather/sensor.py b/custom_components/two_gis_weather/sensor.py new file mode 100644 index 0000000..f038a46 --- /dev/null +++ b/custom_components/two_gis_weather/sensor.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import UnitOfPressure, UnitOfSpeed, UnitOfTemperature +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import TwoGisWeatherUpdateCoordinator + + +SENSOR_SPECS = [ + ("feels_like", "Feels like", UnitOfTemperature.CELSIUS), + ("wind_speed", "Wind speed", UnitOfSpeed.METERS_PER_SECOND), + ("wind_bearing", "Wind bearing", None), + ("pressure", "Pressure", UnitOfPressure.HPA), + ("description", "Weather description", None), + ("icon", "2GIS icon", None), +] + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + coordinator: TwoGisWeatherUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + entities: list[SensorEntity] = [] + for key, name, unit in SENSOR_SPECS: + entities.append(TwoGisWeatherSensor(coordinator, entry, key, name, unit)) + async_add_entities(entities) + + +class TwoGisWeatherSensor(CoordinatorEntity[TwoGisWeatherUpdateCoordinator], SensorEntity): + _attr_has_entity_name = True + + def __init__( + self, + coordinator: TwoGisWeatherUpdateCoordinator, + entry: ConfigEntry, + key: str, + name: str, + unit: str | None, + ) -> None: + super().__init__(coordinator) + self._entry = entry + self._key = key + self._attr_name = name + self._attr_unique_id = f"{entry.entry_id}_sensor_{key}" + self._attr_native_unit_of_measurement = unit + + @property + def native_value(self): + current = (self.coordinator.data or {}).get("current", {}) + return current.get(self._key) + + diff --git a/custom_components/two_gis_weather/strings.json b/custom_components/two_gis_weather/strings.json new file mode 100644 index 0000000..4f052ed --- /dev/null +++ b/custom_components/two_gis_weather/strings.json @@ -0,0 +1,33 @@ +{ + "config": { + "step": { + "user": { + "title": "2GIS Weather", + "description": "Configure 2GIS Weather integration", + "data": { + "api_key": "API key", + "name": "Name", + "latitude": "Latitude", + "longitude": "Longitude", + "language": "Language" + } + } + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid API key", + "unknown": "Unexpected error" + } + }, + "options": { + "step": { + "init": { + "data": { + "update_interval": "Update interval (minutes)", + "language": "Language" + } + } + } + } +} + diff --git a/custom_components/two_gis_weather/translations/en.json b/custom_components/two_gis_weather/translations/en.json new file mode 100644 index 0000000..4f052ed --- /dev/null +++ b/custom_components/two_gis_weather/translations/en.json @@ -0,0 +1,33 @@ +{ + "config": { + "step": { + "user": { + "title": "2GIS Weather", + "description": "Configure 2GIS Weather integration", + "data": { + "api_key": "API key", + "name": "Name", + "latitude": "Latitude", + "longitude": "Longitude", + "language": "Language" + } + } + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid API key", + "unknown": "Unexpected error" + } + }, + "options": { + "step": { + "init": { + "data": { + "update_interval": "Update interval (minutes)", + "language": "Language" + } + } + } + } +} + diff --git a/custom_components/two_gis_weather/weather.py b/custom_components/two_gis_weather/weather.py new file mode 100644 index 0000000..bde9f9e --- /dev/null +++ b/custom_components/two_gis_weather/weather.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +from typing import Any + +from homeassistant.components.weather import ( + WeatherEntity, + WeatherEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import TwoGisWeatherUpdateCoordinator + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + coordinator: TwoGisWeatherUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities([TwoGisWeatherEntity(coordinator, entry)]) + + +class TwoGisWeatherEntity(CoordinatorEntity[TwoGisWeatherUpdateCoordinator], WeatherEntity): + _attr_has_entity_name = True + _attr_name = None + _attr_supported_features = ( + WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY + ) + + def __init__(self, coordinator: TwoGisWeatherUpdateCoordinator, entry: ConfigEntry) -> None: + super().__init__(coordinator) + self._entry = entry + self._attr_unique_id = f"{entry.entry_id}_weather" + self._attr_name = entry.data.get("name") or "2GIS Weather" + + @property + def native_temperature(self) -> float | None: + current = self.coordinator.data.get("current") if self.coordinator.data else None + return (current or {}).get("temperature") + + @property + def native_temperature_unit(self) -> str: + return "°C" + + @property + def condition(self) -> str | None: + current = self.coordinator.data.get("current") if self.coordinator.data else None + return (current or {}).get("condition") + + @property + def native_wind_speed(self) -> float | None: + current = (self.coordinator.data or {}).get("current", {}) + return current.get("wind_speed") + + @property + def native_wind_speed_unit(self) -> str: + return "m/s" + + @property + def wind_bearing(self) -> str | None: + current = (self.coordinator.data or {}).get("current", {}) + return current.get("wind_bearing") + + @property + def native_pressure(self) -> float | None: + current = (self.coordinator.data or {}).get("current", {}) + return current.get("pressure") + + @property + def native_pressure_unit(self) -> str: + return "hPa" + + async def async_forecast_daily(self) -> list[dict[str, Any]] | None: + return (self.coordinator.data or {}).get("daily") + + async def async_forecast_hourly(self) -> list[dict[str, Any]] | None: + return (self.coordinator.data or {}).get("hourly") + +