Skip to content

Commit

Permalink
Add ability to show and hide entities position on map. (close #209)
Browse files Browse the repository at this point in the history
  • Loading branch information
Limych committed Oct 5, 2024
1 parent f62bab4 commit bfc4693
Show file tree
Hide file tree
Showing 11 changed files with 194 additions and 170 deletions.
2 changes: 1 addition & 1 deletion .ruff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
38 changes: 23 additions & 15 deletions custom_components/gismeteo/__init__.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,33 @@
# Copyright (c) 2019-2024, Andrey "Limych" Khrolenok <[email protected]>
# 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 (
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -72,7 +77,7 @@
)


def deslugify(text: str):
def deslugify(text: str) -> str:
"""Deslugify string."""
return text.replace("_", " ").capitalize()

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)

Expand All @@ -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)

Expand All @@ -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
44 changes: 23 additions & 21 deletions custom_components/gismeteo/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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:
Expand All @@ -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

Expand Down Expand Up @@ -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]:
Expand Down Expand Up @@ -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."
Expand Down
56 changes: 39 additions & 17 deletions custom_components/gismeteo/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,30 @@
# Copyright (c) 2019-2021, Andrey "Limych" Khrolenok <[email protected]>
# 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
Expand All @@ -29,19 +36,24 @@

_LOGGER = logging.getLogger(__name__)

type ConfigType = Mapping[str, Any] | None


class GismeteoFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow for Gismeteo."""

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.
Expand All @@ -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 = {}
Expand All @@ -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(
Expand All @@ -99,27 +113,31 @@ 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)


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:
Expand All @@ -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,
}
Expand All @@ -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
Expand Down
Loading

0 comments on commit bfc4693

Please sign in to comment.