Initialize docker stack repo
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
"""Yandex.Pogoda custom integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .config_flow import get_value
|
||||
from .const import (
|
||||
DEFAULT_UPDATES_PER_DAY,
|
||||
DOMAIN,
|
||||
ENTRY_NAME,
|
||||
PLATFORMS,
|
||||
UPDATE_LISTENER,
|
||||
UPDATER,
|
||||
UPDATES_PER_DAY,
|
||||
UPDATES_PER_DAY_MAP,
|
||||
)
|
||||
from .updater import WeatherUpdater
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up entry configured via user interface."""
|
||||
name = get_value(entry, CONF_NAME)
|
||||
api_key = get_value(entry, CONF_API_KEY)
|
||||
latitude = get_value(entry, CONF_LATITUDE, hass.config.latitude)
|
||||
longitude = get_value(entry, CONF_LONGITUDE, hass.config.longitude)
|
||||
updates_per_day = get_value(entry, UPDATES_PER_DAY, DEFAULT_UPDATES_PER_DAY)
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
device_data = hass.data[DOMAIN].get(entry.unique_id, {})
|
||||
weather_data = device_data.get("weather_data")
|
||||
device_data["weather_data"] = None
|
||||
|
||||
weather_updater = WeatherUpdater(
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
api_key=api_key,
|
||||
hass=hass,
|
||||
device_id=entry.unique_id,
|
||||
name=name,
|
||||
weather_data=weather_data,
|
||||
updates_per_day=UPDATES_PER_DAY_MAP[updates_per_day],
|
||||
)
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
ENTRY_NAME: name,
|
||||
UPDATER: weather_updater,
|
||||
}
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
update_listener = entry.add_update_listener(async_update_options)
|
||||
hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER] = update_listener
|
||||
|
||||
if weather_updater.is_first_update_requred:
|
||||
await weather_updater.async_request_refresh()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Update options for entry that was configured via user interface."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Remove entry configured via user interface."""
|
||||
unload_ok = True
|
||||
for platform in PLATFORMS:
|
||||
unload_ok = unload_ok & await hass.config_entries.async_forward_entry_unload(
|
||||
entry=entry, domain=platform
|
||||
)
|
||||
if unload_ok:
|
||||
update_listener = hass.data[DOMAIN][entry.entry_id][UPDATE_LISTENER]
|
||||
update_listener()
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
@@ -0,0 +1,153 @@
|
||||
"""Configuration flows."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import voluptuous as vol
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from .const import (
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
UPDATES_PER_DAY,
|
||||
UPDATES_PER_DAY_SELECTOR,
|
||||
DEFAULT_UPDATES_PER_DAY,
|
||||
)
|
||||
from .updater import WeatherUpdater
|
||||
|
||||
|
||||
def get_value(config_entry: config_entries | None, param: str, default=None):
|
||||
"""Get current value for configuration parameter.
|
||||
|
||||
:param config_entry: config_entries|None: config entry from Flow
|
||||
:param param: str: parameter name for getting value
|
||||
:param default: default value for parameter, defaults to None
|
||||
:returns: parameter value, or default value or None
|
||||
"""
|
||||
if config_entry is not None:
|
||||
return config_entry.options.get(param, config_entry.data.get(param, default))
|
||||
|
||||
return default
|
||||
|
||||
|
||||
class YandexWeatherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""First time set up flow."""
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry):
|
||||
"""Get the options flow for this handler."""
|
||||
return YandexWeatherOptionsFlow()
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle a flow initialized by the user."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
latitude = user_input[CONF_LATITUDE]
|
||||
longitude = user_input[CONF_LONGITUDE]
|
||||
api_key = user_input[CONF_API_KEY].strip()
|
||||
await self.async_set_unique_id(f"{uuid.uuid4()}")
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
weather = await _is_online(api_key, latitude, longitude, self.hass)
|
||||
if weather.last_update_success:
|
||||
user_input[CONF_API_KEY] = api_key
|
||||
|
||||
self.hass.data.setdefault(DOMAIN, {})
|
||||
self.hass.data[DOMAIN][self.unique_id] = {
|
||||
"weather_data": weather.weather_data
|
||||
}
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_NAME], data=user_input
|
||||
)
|
||||
|
||||
errors["base"] = "could_not_get_data"
|
||||
|
||||
schema = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): str,
|
||||
vol.Required(
|
||||
CONF_LATITUDE, default=self.hass.config.latitude
|
||||
): cv.latitude,
|
||||
vol.Required(
|
||||
CONF_LONGITUDE, default=self.hass.config.longitude
|
||||
): cv.longitude,
|
||||
vol.Required(
|
||||
UPDATES_PER_DAY, default=DEFAULT_UPDATES_PER_DAY
|
||||
): UPDATES_PER_DAY_SELECTOR,
|
||||
}
|
||||
)
|
||||
|
||||
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
|
||||
|
||||
|
||||
class YandexWeatherOptionsFlow(config_entries.OptionsFlow):
|
||||
"""Changing options flow."""
|
||||
|
||||
@property
|
||||
def config_entry(self):
|
||||
return self.hass.config_entries.async_get_entry(self.handler)
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Manage the options."""
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
api_key = user_input[CONF_API_KEY].strip()
|
||||
weather = await _is_online(
|
||||
api_key,
|
||||
user_input[CONF_LATITUDE],
|
||||
user_input[CONF_LONGITUDE],
|
||||
self.hass,
|
||||
)
|
||||
if weather.last_update_success:
|
||||
user_input[CONF_API_KEY] = api_key
|
||||
|
||||
self.hass.data.setdefault(DOMAIN, {})
|
||||
self.hass.data[DOMAIN][self.config_entry.unique_id] = {
|
||||
"weather_data": weather.weather_data
|
||||
}
|
||||
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
errors["base"] = "could_not_get_data"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init", data_schema=self._get_options_schema(), errors=errors
|
||||
)
|
||||
|
||||
def _get_options_schema(self):
|
||||
return vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_API_KEY, default=get_value(self.config_entry, CONF_API_KEY)
|
||||
): str,
|
||||
vol.Required(
|
||||
CONF_LATITUDE, default=get_value(self.config_entry, CONF_LATITUDE)
|
||||
): cv.latitude,
|
||||
vol.Required(
|
||||
CONF_LONGITUDE, default=get_value(self.config_entry, CONF_LONGITUDE)
|
||||
): cv.longitude,
|
||||
vol.Required(
|
||||
UPDATES_PER_DAY,
|
||||
default=get_value(
|
||||
self.config_entry,
|
||||
UPDATES_PER_DAY,
|
||||
self.config_entry.data.get(
|
||||
UPDATES_PER_DAY, DEFAULT_UPDATES_PER_DAY
|
||||
),
|
||||
),
|
||||
): UPDATES_PER_DAY_SELECTOR,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def _is_online(api_key, lat, lon, hass: HomeAssistant) -> bool:
|
||||
weather = WeatherUpdater(lat, lon, api_key, hass, f"{uuid.uuid4()}")
|
||||
await weather.async_request_refresh()
|
||||
return weather
|
||||
261
homeassistant/config/custom_components/yandex_pogoda/const.py
Normal file
261
homeassistant/config/custom_components/yandex_pogoda/const.py
Normal file
@@ -0,0 +1,261 @@
|
||||
"""General constants."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import logging
|
||||
import pickle
|
||||
import zlib
|
||||
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_WEATHER_TEMPERATURE_UNIT,
|
||||
ATTR_WEATHER_WIND_SPEED_UNIT,
|
||||
UNIT_CONVERSIONS,
|
||||
)
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.helpers import selector
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "yandex_pogoda"
|
||||
DEFAULT_NAME = "Yandex Pogoda"
|
||||
ATTRIBUTION = "Data provided by Yandex Pogoda"
|
||||
MANUFACTURER = "Yandex"
|
||||
ENTRY_NAME = "name"
|
||||
UPDATER = "updater"
|
||||
|
||||
ATTR_API_TEMPERATURE = "temperature"
|
||||
ATTR_API_TEMPERATURE_MAX = "maxTemperature"
|
||||
ATTR_API_TEMPERATURE_MIN = "minTemperature"
|
||||
ATTR_API_FEELS_LIKE_TEMPERATURE = "feelsLike"
|
||||
ATTR_API_WIND_SPEED = "windSpeed"
|
||||
ATTR_API_WIND_BEARING = "windAngle"
|
||||
ATTR_API_DAYTIME = "daytime"
|
||||
ATTR_API_CONDITION = "condition"
|
||||
ATTR_API_IMAGE = "icon"
|
||||
ATTR_API_POLAR = "polar"
|
||||
ATTR_API_SERVER_TIME = "serverTime"
|
||||
ATTR_API_TIME = "time"
|
||||
ATTR_API_SUNRISE_BEGIN_TIME = "sunriseTime"
|
||||
ATTR_API_SUNRISE_END_TIME = "sunsetTime"
|
||||
ATTR_API_YA_CONDITION = "yandex_condition"
|
||||
ATTR_API_WIND_GUST = "windGust"
|
||||
|
||||
ATTR_FORECAST_DATA = "forecast"
|
||||
ATTR_FORECAST_DATA_COMPRESSED = "forecast_compressed"
|
||||
ATTR_FORECAST_HOURLY = "hourly"
|
||||
ATTR_FORECAST_HOURLY_ICONS = "forecast_hourly_icons"
|
||||
ATTR_FORECAST_TWICE_DAILY = "twice_daily"
|
||||
ATTR_FORECAST_TWICE_DAILY_ICONS = "forecast_twice_daily_icons"
|
||||
|
||||
ATTR_MIN_FORECAST_TEMPERATURE = "min_forecast_temperature"
|
||||
ATTR_WIND_INTERCARDINAL_DIRECTION = "wind_intercardinal"
|
||||
|
||||
UPDATE_LISTENER = "update_listener"
|
||||
PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
|
||||
|
||||
YA_CONDITIONS = [
|
||||
"CLEAR",
|
||||
"PARTLY_CLOUDY",
|
||||
"CLOUDY",
|
||||
"OVERCAST",
|
||||
"LIGHT_RAIN",
|
||||
"RAIN",
|
||||
"HEAVY_RAIN",
|
||||
"SHOWERS",
|
||||
"SLEET",
|
||||
"LIGHT_SNOW",
|
||||
"SNOW",
|
||||
"SNOWFALL",
|
||||
"HAIL",
|
||||
"THUNDERSTORM",
|
||||
"THUNDERSTORM_WITH_RAIN",
|
||||
"THUNDERSTORM_WITH_HAIL",
|
||||
]
|
||||
YA_CONDITION_STATE_MAP = {i: i.lower() for i in YA_CONDITIONS}
|
||||
"""Mapping for yandex states"""
|
||||
|
||||
YA_CONDITION_STATES = list(YA_CONDITION_STATE_MAP.values())
|
||||
|
||||
WEATHER_STATES_CONVERSION = {
|
||||
"CLEAR": {
|
||||
"day": "sunny",
|
||||
"night": "clear-night",
|
||||
},
|
||||
"PARTLY_CLOUDY": "partlycloudy",
|
||||
"CLOUDY": "cloudy",
|
||||
"OVERCAST": "cloudy",
|
||||
"LIGHT_RAIN": "rainy",
|
||||
"RAIN": "rainy",
|
||||
"HEAVY_RAIN": "pouring",
|
||||
"SHOWERS": "pouring",
|
||||
"SLEET": "snowy-rainy",
|
||||
"LIGHT_SNOW": "snowy",
|
||||
"SNOW": "snowy",
|
||||
"SNOWFALL": "snowy",
|
||||
"HAIL": "hail",
|
||||
"THUNDERSTORM": "lightning",
|
||||
"THUNDERSTORM_WITH_RAIN": "lightning-rainy",
|
||||
"THUNDERSTORM_WITH_HAIL": "lightning-rainy",
|
||||
}
|
||||
"""Map rich Yandex weather condition to ordinary HA"""
|
||||
|
||||
HA_WEATHER_STATES = [
|
||||
i for i in WEATHER_STATES_CONVERSION.values() if isinstance(i, str)
|
||||
] + list(WEATHER_STATES_CONVERSION["CLEAR"].values())
|
||||
|
||||
CONDITION_ICONS = {
|
||||
"CLEAR": {
|
||||
"day": "mdi:weather-sunny",
|
||||
"night": "mdi:weather-night",
|
||||
},
|
||||
"PARTLY_CLOUDY": {
|
||||
"day": "mdi:weather-partly-cloudy",
|
||||
"night": "mdi:weather-night-partly-cloudy",
|
||||
},
|
||||
"CLOUDY": "mdi:weather-cloudy",
|
||||
"OVERCAST": "mdi:weather-cloudy",
|
||||
"LIGHT_RAIN": "mdi:weather-rainy",
|
||||
"RAIN": "mdi:weather-rainy",
|
||||
"HEAVY_RAIN": "mdi:weather-pouring",
|
||||
"SHOWERS": "mdi:weather-pouring",
|
||||
"SLEET": "mdi:weather-snowy-rainy",
|
||||
"LIGHT_SNOW": "mdi:weather-snowy",
|
||||
"SNOW": "mdi:weather-snowy",
|
||||
"SNOWFALL": "mdi:weather-snowy-heavy",
|
||||
"HAIL": "mdi:weather-hail",
|
||||
"THUNDERSTORM": "mdi:weather-lightning",
|
||||
"THUNDERSTORM_WITH_RAIN": "mdi:weather-lightning-rainy",
|
||||
"THUNDERSTORM_WITH_HAIL": "mdi:weather-lightning-rainy",
|
||||
}
|
||||
"""Mapping for state icon"""
|
||||
|
||||
|
||||
TEMPERATURE_CONVERTER = UNIT_CONVERSIONS[ATTR_WEATHER_TEMPERATURE_UNIT]
|
||||
WIND_SPEED_CONVERTER = UNIT_CONVERSIONS[ATTR_WEATHER_WIND_SPEED_UNIT]
|
||||
|
||||
DEFAULT_UPDATES_PER_DAY = "standard"
|
||||
UPDATES_PER_DAY_MAP = {
|
||||
"rare": 6,
|
||||
"standard": 12,
|
||||
"often": 24,
|
||||
}
|
||||
UPDATES_PER_DAY = "updates_per_day"
|
||||
UPDATES_PER_DAY_SELECTOR = selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=["rare", "standard", "often"],
|
||||
mode=selector.SelectSelectorMode.DROPDOWN,
|
||||
translation_key=UPDATES_PER_DAY,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def convert_unit_value(
|
||||
converter: Callable[[float, str, str], float],
|
||||
val: float | None,
|
||||
unit_from: str | None,
|
||||
unit_to: str | None,
|
||||
) -> float | None:
|
||||
"""Weather factor unit converter."""
|
||||
if val is not None and unit_from and unit_to:
|
||||
return converter(val, unit_from, unit_to)
|
||||
return None
|
||||
|
||||
|
||||
def map_state(src: str, is_day: bool, mapping: dict | None = None) -> str:
|
||||
"""Map weather condition based on WEATHER_STATES_CONVERSION.
|
||||
|
||||
:param src: str: Yandex weather state
|
||||
:param is_day: bool: Is it day? Used for icons
|
||||
:param mapping: use this dict for mapping
|
||||
:return: str: Home Assistant weather state
|
||||
"""
|
||||
if mapping is None:
|
||||
mapping = {}
|
||||
|
||||
try:
|
||||
result: str | dict[str, str] = mapping[src]
|
||||
except KeyError:
|
||||
result = src
|
||||
|
||||
if type(result) is dict:
|
||||
return result["day" if is_day else "night"]
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_wind_intercardinal_direction(wind_direction_degree: int) -> str:
|
||||
"""Convert wind direction degree to named direction."""
|
||||
|
||||
if wind_direction_degree < 0 or wind_direction_degree > 360:
|
||||
_LOGGER.error("Angle should be in [0, 360] range")
|
||||
|
||||
if wind_direction_degree <= 22.5:
|
||||
return "n"
|
||||
|
||||
if wind_direction_degree <= 67.5:
|
||||
return "ne"
|
||||
|
||||
if wind_direction_degree <= 112.5:
|
||||
return "e"
|
||||
|
||||
if wind_direction_degree <= 157.5:
|
||||
return "se"
|
||||
|
||||
if wind_direction_degree <= 202.5:
|
||||
return "s"
|
||||
|
||||
if wind_direction_degree <= 247.5:
|
||||
return "sw"
|
||||
|
||||
if wind_direction_degree <= 292.5:
|
||||
return "w"
|
||||
|
||||
if wind_direction_degree <= 337.5:
|
||||
return "nw"
|
||||
|
||||
return "n"
|
||||
|
||||
|
||||
def compress_data(data: dict) -> str:
|
||||
"""Compress dict to string."""
|
||||
return base64.b64encode(zlib.compress(pickle.dumps(data))).decode("utf-8")
|
||||
|
||||
|
||||
def decompress_data(compressed: str | None) -> dict:
|
||||
"""Compress dict to string."""
|
||||
if not compressed:
|
||||
return {}
|
||||
|
||||
try:
|
||||
return pickle.loads(zlib.decompress(base64.b64decode(compressed)))
|
||||
except TypeError: # for backward compatibility
|
||||
return {}
|
||||
|
||||
|
||||
def is_daytime(
|
||||
hour: datetime,
|
||||
day: datetime,
|
||||
sunrise_begin: datetime,
|
||||
sunset_end: datetime,
|
||||
polar: str,
|
||||
) -> bool:
|
||||
"""returns whether it is daytime for a given hour of the day"""
|
||||
|
||||
if polar:
|
||||
if day == sunrise_begin and day == sunset_end:
|
||||
return polar == "DAY"
|
||||
|
||||
if day == sunrise_begin:
|
||||
return hour < sunset_end
|
||||
|
||||
if day == sunset_end:
|
||||
return hour >= sunrise_begin
|
||||
|
||||
if sunrise_begin <= sunset_end:
|
||||
return hour >= sunrise_begin and hour < sunset_end
|
||||
else:
|
||||
return hour < sunset_end or hour >= sunrise_begin
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Yandex.Pogoda triggers."""
|
||||
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
from homeassistant.components.device_automation import \
|
||||
DEVICE_TRIGGER_BASE_SCHEMA as HA_TRIGGER_BASE_SCHEMA
|
||||
from homeassistant.components.homeassistant.triggers import \
|
||||
event as event_trigger
|
||||
from homeassistant.const import (CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM,
|
||||
CONF_TYPE)
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN, WEATHER_STATES_CONVERSION, map_state
|
||||
|
||||
|
||||
def generate_triggers() -> list:
|
||||
"""Generate triggers list."""
|
||||
result = []
|
||||
for s in WEATHER_STATES_CONVERSION:
|
||||
for d in [True, False]:
|
||||
result.append(map_state(src=s, is_day=d, mapping=WEATHER_STATES_CONVERSION))
|
||||
|
||||
return list(set(result))
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
TRIGGERS = generate_triggers()
|
||||
|
||||
TRIGGER_SCHEMA = HA_TRIGGER_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_TYPE): vol.In(TRIGGERS),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def async_get_triggers(_hass: HomeAssistant, device_id):
|
||||
"""Return a list of triggers."""
|
||||
triggers = []
|
||||
|
||||
for t in TRIGGERS:
|
||||
triggers.append(
|
||||
{
|
||||
# Required fields of TRIGGER_BASE_SCHEMA
|
||||
CONF_PLATFORM: "device",
|
||||
CONF_DOMAIN: DOMAIN,
|
||||
CONF_DEVICE_ID: device_id,
|
||||
# Required fields of TRIGGER_SCHEMA
|
||||
CONF_TYPE: t,
|
||||
}
|
||||
)
|
||||
|
||||
return triggers
|
||||
|
||||
|
||||
async def async_attach_trigger(hass: HomeAssistant, config, action, automation_info):
|
||||
"""Attach a trigger."""
|
||||
config = TRIGGER_SCHEMA(config)
|
||||
_LOGGER.debug("Got subscription to trigger: %s", config)
|
||||
event_config = event_trigger.TRIGGER_SCHEMA(
|
||||
{
|
||||
event_trigger.CONF_PLATFORM: "event",
|
||||
event_trigger.CONF_EVENT_TYPE: DOMAIN + "_event",
|
||||
event_trigger.CONF_EVENT_DATA: {
|
||||
CONF_DEVICE_ID: config[CONF_DEVICE_ID],
|
||||
CONF_TYPE: config[CONF_TYPE],
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
return await event_trigger.async_attach_trigger(
|
||||
hass, event_config, action, automation_info, platform_type="device"
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"domain": "yandex_pogoda",
|
||||
"name": "Yandex Pogoda",
|
||||
"codeowners": [],
|
||||
"config_flow": true,
|
||||
"documentation": "https://github.com/yandex/pogoda-home-assistant/wiki",
|
||||
"iot_class": "cloud_polling",
|
||||
"issue_tracker": "https://github.com/yandex/pogoda-home-assistant/issues",
|
||||
"requirements": [],
|
||||
"version": "0.0.10"
|
||||
}
|
||||
250
homeassistant/config/custom_components/yandex_pogoda/sensor.py
Normal file
250
homeassistant/config/custom_components/yandex_pogoda/sensor.py
Normal file
@@ -0,0 +1,250 @@
|
||||
"""Sensor component."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dateutil import parser
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
DEGREE,
|
||||
UnitOfSpeed,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import EntityCategory
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import (
|
||||
ATTR_API_CONDITION,
|
||||
ATTR_API_FEELS_LIKE_TEMPERATURE,
|
||||
ATTR_API_TEMPERATURE,
|
||||
ATTR_API_SERVER_TIME,
|
||||
ATTR_API_WIND_BEARING,
|
||||
ATTR_WIND_INTERCARDINAL_DIRECTION,
|
||||
ATTR_API_WIND_SPEED,
|
||||
ATTR_API_WIND_GUST,
|
||||
ATTR_API_YA_CONDITION,
|
||||
ATTR_MIN_FORECAST_TEMPERATURE,
|
||||
ATTR_API_SUNRISE_BEGIN_TIME,
|
||||
ATTR_API_SUNRISE_END_TIME,
|
||||
ATTRIBUTION,
|
||||
DOMAIN,
|
||||
ENTRY_NAME,
|
||||
HA_WEATHER_STATES,
|
||||
TEMPERATURE_CONVERTER,
|
||||
UPDATER,
|
||||
WIND_SPEED_CONVERTER,
|
||||
YA_CONDITION_STATES,
|
||||
WEATHER_STATES_CONVERSION,
|
||||
convert_unit_value,
|
||||
)
|
||||
from .updater import WeatherUpdater
|
||||
|
||||
UNIT_CONVERTOR_TYPE_MAP: dict[str, str] = {
|
||||
ATTR_API_TEMPERATURE: TEMPERATURE_CONVERTER,
|
||||
ATTR_API_FEELS_LIKE_TEMPERATURE: TEMPERATURE_CONVERTER,
|
||||
ATTR_API_WIND_SPEED: WIND_SPEED_CONVERTER,
|
||||
ATTR_API_WIND_GUST: WIND_SPEED_CONVERTER,
|
||||
ATTR_MIN_FORECAST_TEMPERATURE: TEMPERATURE_CONVERTER,
|
||||
}
|
||||
|
||||
WEATHER_SENSORS: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_TEMPERATURE,
|
||||
name="Temperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_registry_enabled_default=True,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_FEELS_LIKE_TEMPERATURE,
|
||||
name="Feels like temperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_registry_enabled_default=True,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_WIND_SPEED,
|
||||
name="Wind speed",
|
||||
native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_registry_enabled_default=True,
|
||||
icon="mdi:weather-windy",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_WIND_GUST,
|
||||
name="Wind gust",
|
||||
native_unit_of_measurement=UnitOfSpeed.METERS_PER_SECOND,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_registry_enabled_default=True,
|
||||
icon="mdi:weather-windy",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_WIND_BEARING,
|
||||
name="Wind bearing",
|
||||
entity_registry_enabled_default=False,
|
||||
icon="mdi:compass-rose",
|
||||
translation_key=ATTR_API_WIND_BEARING,
|
||||
native_unit_of_measurement=DEGREE,
|
||||
device_class=SensorDeviceClass.WIND_DIRECTION,
|
||||
state_class=SensorStateClass.MEASUREMENT_ANGLE,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_WIND_INTERCARDINAL_DIRECTION,
|
||||
name="Wind direction",
|
||||
entity_registry_enabled_default=True,
|
||||
icon="mdi:compass-rose",
|
||||
translation_key=ATTR_WIND_INTERCARDINAL_DIRECTION,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_CONDITION,
|
||||
name="Condition HomeAssistant",
|
||||
entity_registry_enabled_default=False,
|
||||
translation_key=ATTR_API_CONDITION,
|
||||
options=HA_WEATHER_STATES,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_SERVER_TIME,
|
||||
name="Data update time",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
state_class=None,
|
||||
entity_registry_enabled_default=True,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_YA_CONDITION,
|
||||
name="Condition Yandex",
|
||||
entity_registry_enabled_default=True,
|
||||
translation_key=ATTR_API_YA_CONDITION,
|
||||
options=YA_CONDITION_STATES,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_MIN_FORECAST_TEMPERATURE,
|
||||
name="Minimal forecast temperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_registry_enabled_default=True,
|
||||
icon="mdi:thermometer-chevron-down",
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_SUNRISE_BEGIN_TIME,
|
||||
name="Next sunrise",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
entity_registry_enabled_default=True,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=ATTR_API_SUNRISE_END_TIME,
|
||||
name="Next sunset",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
entity_registry_enabled_default=True,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up weather "Yandex.Weather" sensor entry."""
|
||||
domain_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
name = domain_data[ENTRY_NAME]
|
||||
updater = domain_data[UPDATER]
|
||||
|
||||
entities: list[YandexWeatherSensor] = [
|
||||
YandexWeatherSensor(
|
||||
name,
|
||||
f"{config_entry.unique_id}-{description.key}",
|
||||
description,
|
||||
updater,
|
||||
)
|
||||
for description in WEATHER_SENSORS
|
||||
]
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class YandexWeatherSensor(SensorEntity, CoordinatorEntity, RestoreEntity):
|
||||
"""Yandex.Weather sensor entry."""
|
||||
|
||||
_attr_attribution = ATTRIBUTION
|
||||
coordinator: WeatherUpdater
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
unique_id: str,
|
||||
description: SensorEntityDescription,
|
||||
updater: WeatherUpdater,
|
||||
) -> None:
|
||||
"""Initialize sensor."""
|
||||
CoordinatorEntity.__init__(self, coordinator=updater)
|
||||
RestoreEntity.__init__(self)
|
||||
self.entity_description = description
|
||||
|
||||
self._attr_name = f"{name} {description.name}"
|
||||
self._attr_unique_id = unique_id
|
||||
self._attr_device_info = self.coordinator.device_info
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to hass."""
|
||||
await RestoreEntity.async_added_to_hass(self)
|
||||
await CoordinatorEntity.async_added_to_hass(self)
|
||||
|
||||
state = await self.async_get_last_state()
|
||||
if not state:
|
||||
return
|
||||
|
||||
if state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
|
||||
self._attr_available = False
|
||||
else:
|
||||
self._attr_available = True
|
||||
if self.entity_description.key in {
|
||||
ATTR_API_SERVER_TIME,
|
||||
ATTR_API_SUNRISE_BEGIN_TIME,
|
||||
ATTR_API_SUNRISE_END_TIME,
|
||||
}:
|
||||
self._attr_native_value = parser.parse(state.state)
|
||||
elif self.entity_description.key in UNIT_CONVERTOR_TYPE_MAP:
|
||||
self._attr_native_value = str(
|
||||
convert_unit_value(
|
||||
UNIT_CONVERTOR_TYPE_MAP[self.entity_description.key],
|
||||
float(state.state),
|
||||
state.attributes.get("unit_of_measurement")
|
||||
or self.unit_of_measurement,
|
||||
self.native_unit_of_measurement,
|
||||
)
|
||||
)
|
||||
elif (
|
||||
self.entity_description.key == ATTR_API_YA_CONDITION
|
||||
and state.state not in YA_CONDITION_STATES
|
||||
): # for backward compatibility
|
||||
self._attr_native_value = WEATHER_STATES_CONVERSION.get(state.state)
|
||||
else:
|
||||
self._attr_native_value = state.state
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
self._attr_available = True
|
||||
self._attr_native_value = self.coordinator.data.get(
|
||||
self.entity_description.key, None
|
||||
)
|
||||
if self.entity_description.key == ATTR_API_YA_CONDITION:
|
||||
self._attr_icon = self.coordinator.data.get(f"{ATTR_API_YA_CONDITION}_icon")
|
||||
|
||||
self.async_write_ha_state()
|
||||
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"options": {
|
||||
"abort": {
|
||||
"already_configured": "This location is already used for Yandex.Pogoda."
|
||||
},
|
||||
"error": {
|
||||
"could_not_get_data": "Could not get data. Check your API key in Yandex developer portal. It may took up to 5 minutes for updating key settings at Yandex side."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Yandex.Pogoda settings",
|
||||
"description": "Settings for Yandex Pogoda integration.\n\nCheck [Yandex.Pogoda API for Smart Home](https://yandex.ru/pogoda/b2b/smarthome) to get your API key",
|
||||
"data": {
|
||||
"api_key": "Yandex.Pogoda API key",
|
||||
"latitude": "Latitude for weather info (default is HA latitude)",
|
||||
"longitude": "Longitude for weather info (default is HA longitude)",
|
||||
"language": "Language for Yandex weather state sensor",
|
||||
"updates_per_day": "Updates per day",
|
||||
"image_source": "Weather condition images"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "This location is already used for Yandex.Pogoda."
|
||||
},
|
||||
"error": {
|
||||
"could_not_get_data": "Could not get data. Check your API key in Yandex developer portal. It may took up to 5 minutes for updating key settings at Yandex side."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Yandex.Pogoda settings",
|
||||
"description": "Settings for Yandex Pogoda integration.\n\nCheck [Yandex.Pogoda API for Smart Home](https://yandex.ru/pogoda/b2b/smarthome) to get your API key",
|
||||
"data": {
|
||||
"api_key": "Yandex.Pogoda API key",
|
||||
"name": "Name",
|
||||
"latitude": "Latitude for weather info (default is HA latitude)",
|
||||
"longitude": "Longitude for weather info (default is HA longitude)",
|
||||
"language": "Language for Yandex weather state sensor",
|
||||
"updates_per_day": "Updates per day",
|
||||
"image_source": "Weather condition images"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"updates_per_day": {
|
||||
"options": {
|
||||
"rare": "Rare (6 times per day)",
|
||||
"standard": "Standard (12 times per day)",
|
||||
"often": "Often (24 times per day)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Yandex Pogoda",
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"wind_intercardinal": {
|
||||
"state": {
|
||||
"ne": "NE",
|
||||
"e": "E",
|
||||
"se": "SE",
|
||||
"s": "S",
|
||||
"sw": "SW",
|
||||
"w": "W",
|
||||
"nw": "NW",
|
||||
"n": "N"
|
||||
}
|
||||
},
|
||||
"yandex_condition": {
|
||||
"state": {
|
||||
"clear": "Clear",
|
||||
"partly_cloudy": "Partly cloudy",
|
||||
"cloudy": "Cloudy",
|
||||
"overcast": "Overcast",
|
||||
"light_rain": "Light rain",
|
||||
"rain": "Rain",
|
||||
"heavy_rain": "Heavy rain",
|
||||
"showers": "Showers",
|
||||
"sleet": "Sleet",
|
||||
"light_snow": "Light snow",
|
||||
"snow": "Snow",
|
||||
"snowfall": "Snowfall",
|
||||
"hail": "Hail",
|
||||
"thunderstorm": "Thunderstorm",
|
||||
"thunderstorm_with_rain": "Thunderstorm with rain",
|
||||
"thunderstorm_with_hail": "Thunderstorm with hail"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"options": {
|
||||
"abort": {
|
||||
"already_configured": "Выбранное местоположение уже используется для получения погодных данных Яндекс.Погоды."
|
||||
},
|
||||
"error": {
|
||||
"could_not_get_data": "Не могу получить данные. Проверьте корректность API-ключа в кабинете разработчика. Может потребоваться до 5 минут, чтобы изменения настроек ключа вступили в силу на стороне Яндекса."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Настройки Яндекс.Погоды",
|
||||
"description": "Настройки интеграции Яндекс.Погоды.\n\nДля получения API ключа посетите [API Яндекс.Погоды для умного дома](https://yandex.ru/pogoda/b2b/smarthome)",
|
||||
"data": {
|
||||
"api_key": "API ключ погоды",
|
||||
"latitude": "Широта точки где нужно узнавать погоду (по умолчанию -- широта Home Assistant)",
|
||||
"longitude": "Долгота точки где нужно узнавать погоду (по умолчанию -- долгота Home Assistant)",
|
||||
"language": "На каком языке сообщать состояние погоды в сенсоре текущей погоды",
|
||||
"updates_per_day": "Обновлений в день",
|
||||
"image_source": "Картинки состояния погоды"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Выбранное местоположение уже используется для получения погодных данных Яндекс.Погоды."
|
||||
},
|
||||
"error": {
|
||||
"could_not_get_data": "Не могу получить данные. Проверьте корректность API-ключа в кабинете разработчика. Может потребоваться до 5 минут, чтобы изменения настроек ключа вступили в силу на стороне Яндекса."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Настройки Яндекс.Погоды",
|
||||
"description": "Настройки интеграции Яндекс.Погоды.\n\nДля получения API ключа посетите [API Яндекс.Погоды для умного дома](https://yandex.ru/pogoda/b2b/smarthome)",
|
||||
"data": {
|
||||
"api_key": "API ключ погоды",
|
||||
"name": "Название",
|
||||
"latitude": "Широта точки где нужно узнавать погоду (по умолчанию -- широта Home Assistant)",
|
||||
"longitude": "Долгота точки где нужно узнавать погоду (по умолчанию -- долгота Home Assistant)",
|
||||
"language": "На каком языке сообщать состояние погоды в сенсоре текущей погоды",
|
||||
"updates_per_day": "Обновлений в день",
|
||||
"image_source": "Картинки состояния погоды"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"updates_per_day": {
|
||||
"options": {
|
||||
"rare": "Редко (6 раз в сутки)",
|
||||
"standard": "Стандарт (12 раз в сутки)",
|
||||
"often": "Часто (24 раза в сутки)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Яндекс.Погода",
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"wind_intercardinal": {
|
||||
"state": {
|
||||
"ne": "СВ",
|
||||
"e": "В",
|
||||
"se": "ЮВ",
|
||||
"s": "Ю",
|
||||
"sw": "ЮЗ",
|
||||
"w": "З",
|
||||
"nw": "СЗ",
|
||||
"n": "С"
|
||||
}
|
||||
},
|
||||
"yandex_condition": {
|
||||
"state": {
|
||||
"clear": "Ясно",
|
||||
"partly_cloudy": "Малооблачно",
|
||||
"cloudy": "Облачно с прояснениями",
|
||||
"overcast": "Пасмурно",
|
||||
"light_rain": "Слабый дождь",
|
||||
"rain": "Дождь",
|
||||
"heavy_rain": "Сильный дождь",
|
||||
"showers": "Ливень",
|
||||
"sleet": "Снег с дождём",
|
||||
"light_snow": "Слабый снег",
|
||||
"snow": "Снег",
|
||||
"snowfall": "Снегопад",
|
||||
"hail": "Град",
|
||||
"thunderstorm": "Гроза",
|
||||
"thunderstorm_with_rain": "Дождь с грозой",
|
||||
"thunderstorm_with_hail": "Гроза с градом"
|
||||
}
|
||||
},
|
||||
"condition": {
|
||||
"state": {
|
||||
"clear-night": "Ясно, ночь",
|
||||
"cloudy": "Облачно",
|
||||
"exceptional": "Предупреждение",
|
||||
"fog": "Туман",
|
||||
"hail": "Град",
|
||||
"lightning": "Молния",
|
||||
"lightning-rainy": "Молния, дождь",
|
||||
"partlycloudy": "Переменная облачность",
|
||||
"pouring": "Ливень",
|
||||
"rainy": "Дождь",
|
||||
"snowy": "Снег",
|
||||
"snowy-rainy": "Снег с дождем",
|
||||
"sunny": "Ясно",
|
||||
"windy": "Ветрено",
|
||||
"windy-variant": "Ветрено"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
457
homeassistant/config/custom_components/yandex_pogoda/updater.py
Normal file
457
homeassistant/config/custom_components/yandex_pogoda/updater.py
Normal file
@@ -0,0 +1,457 @@
|
||||
"""Pogoda data updater."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import aiohttp
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from dateutil import parser
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_FORECAST_CONDITION,
|
||||
ATTR_FORECAST_NATIVE_APPARENT_TEMP,
|
||||
ATTR_FORECAST_NATIVE_TEMP,
|
||||
ATTR_FORECAST_NATIVE_TEMP_LOW,
|
||||
ATTR_FORECAST_NATIVE_WIND_GUST_SPEED,
|
||||
ATTR_FORECAST_NATIVE_WIND_SPEED,
|
||||
ATTR_FORECAST_WIND_BEARING,
|
||||
Forecast,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, HomeAssistantError, callback
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import (
|
||||
ATTR_API_CONDITION,
|
||||
ATTR_API_DAYTIME,
|
||||
ATTR_API_FEELS_LIKE_TEMPERATURE,
|
||||
ATTR_API_IMAGE,
|
||||
ATTR_API_POLAR,
|
||||
ATTR_API_SERVER_TIME,
|
||||
ATTR_API_SUNRISE_BEGIN_TIME,
|
||||
ATTR_API_SUNRISE_END_TIME,
|
||||
ATTR_API_TEMPERATURE,
|
||||
ATTR_API_TEMPERATURE_MAX,
|
||||
ATTR_API_TEMPERATURE_MIN,
|
||||
ATTR_API_TIME,
|
||||
ATTR_API_WIND_BEARING,
|
||||
ATTR_API_WIND_GUST,
|
||||
ATTR_API_WIND_SPEED,
|
||||
ATTR_API_YA_CONDITION,
|
||||
ATTR_FORECAST_DATA,
|
||||
ATTR_FORECAST_HOURLY,
|
||||
ATTR_FORECAST_HOURLY_ICONS,
|
||||
ATTR_FORECAST_TWICE_DAILY,
|
||||
ATTR_FORECAST_TWICE_DAILY_ICONS,
|
||||
ATTR_MIN_FORECAST_TEMPERATURE,
|
||||
ATTR_WIND_INTERCARDINAL_DIRECTION,
|
||||
DEFAULT_UPDATES_PER_DAY,
|
||||
UPDATES_PER_DAY_MAP,
|
||||
CONDITION_ICONS,
|
||||
DOMAIN,
|
||||
MANUFACTURER,
|
||||
WEATHER_STATES_CONVERSION,
|
||||
YA_CONDITION_STATE_MAP,
|
||||
get_wind_intercardinal_direction,
|
||||
is_daytime,
|
||||
map_state,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
API_URL = "https://api.weather.yandex.ru/graphql/query"
|
||||
API_HEADER_NAME = "X-Yandex-Weather-Key"
|
||||
|
||||
API_QUERY_TEMPLATE = f"""{{
|
||||
{ATTR_API_SERVER_TIME}
|
||||
weatherByPoint(request: {{ lat: %s, lon: %s }}) {{
|
||||
now {{
|
||||
{ATTR_API_CONDITION}
|
||||
{ATTR_API_DAYTIME}
|
||||
{ATTR_API_FEELS_LIKE_TEMPERATURE}
|
||||
{ATTR_API_IMAGE}(format: PNG_64)
|
||||
{ATTR_API_TEMPERATURE}
|
||||
{ATTR_API_WIND_BEARING}
|
||||
{ATTR_API_WIND_GUST}
|
||||
{ATTR_API_WIND_SPEED}
|
||||
}}
|
||||
forecast {{
|
||||
days {{
|
||||
{ATTR_API_POLAR}
|
||||
{ATTR_API_TIME}
|
||||
{ATTR_API_SUNRISE_BEGIN_TIME}
|
||||
{ATTR_API_SUNRISE_END_TIME}
|
||||
hours{{
|
||||
{ATTR_API_TIME}
|
||||
{ATTR_API_CONDITION}
|
||||
{ATTR_API_FEELS_LIKE_TEMPERATURE}
|
||||
{ATTR_API_IMAGE}(format: PNG_64)
|
||||
{ATTR_API_TEMPERATURE}
|
||||
{ATTR_API_WIND_BEARING}
|
||||
{ATTR_API_WIND_GUST}
|
||||
{ATTR_API_WIND_SPEED}
|
||||
}}
|
||||
summary{{
|
||||
night{{
|
||||
...forecastFields
|
||||
}}
|
||||
day{{
|
||||
...forecastFields
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
fragment forecastFields on Daypart {{
|
||||
{ATTR_API_CONDITION}
|
||||
{ATTR_API_FEELS_LIKE_TEMPERATURE}
|
||||
{ATTR_API_IMAGE}(format: PNG_64)
|
||||
{ATTR_API_WIND_BEARING}
|
||||
{ATTR_API_WIND_GUST}
|
||||
{ATTR_API_WIND_SPEED}
|
||||
{ATTR_API_TEMPERATURE_MAX}
|
||||
{ATTR_API_TEMPERATURE_MIN}
|
||||
}}"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class AttributeMapper:
|
||||
"""Attribute mapper."""
|
||||
|
||||
src: str
|
||||
_dst: str | None = None
|
||||
mapping: dict | None = None
|
||||
default: str | float | None = None
|
||||
|
||||
@property
|
||||
def dst(self) -> str:
|
||||
"""Destination for mapping."""
|
||||
return self.src if self._dst is None else self._dst
|
||||
|
||||
|
||||
CURRENT_WEATHER_ATTRIBUTE_TRANSLATION: list[AttributeMapper] = [
|
||||
AttributeMapper(ATTR_API_WIND_BEARING),
|
||||
AttributeMapper(
|
||||
ATTR_API_WIND_BEARING,
|
||||
ATTR_WIND_INTERCARDINAL_DIRECTION,
|
||||
mapping=get_wind_intercardinal_direction,
|
||||
),
|
||||
AttributeMapper(ATTR_API_CONDITION, ATTR_API_YA_CONDITION, YA_CONDITION_STATE_MAP),
|
||||
AttributeMapper(
|
||||
ATTR_API_CONDITION, f"{ATTR_API_YA_CONDITION}_icon", CONDITION_ICONS
|
||||
),
|
||||
AttributeMapper(ATTR_API_CONDITION, mapping=WEATHER_STATES_CONVERSION),
|
||||
AttributeMapper(ATTR_API_FEELS_LIKE_TEMPERATURE),
|
||||
AttributeMapper(ATTR_API_SUNRISE_BEGIN_TIME),
|
||||
AttributeMapper(ATTR_API_SUNRISE_END_TIME),
|
||||
AttributeMapper(ATTR_API_IMAGE),
|
||||
AttributeMapper(ATTR_API_TEMPERATURE),
|
||||
AttributeMapper(ATTR_API_WIND_GUST),
|
||||
AttributeMapper(ATTR_API_WIND_SPEED, default=0),
|
||||
]
|
||||
|
||||
|
||||
FORECAST_HOUR_ATTRIBUTE_TRANSLATION: list[AttributeMapper] = [
|
||||
AttributeMapper(ATTR_API_WIND_BEARING, ATTR_FORECAST_WIND_BEARING),
|
||||
AttributeMapper(ATTR_API_WIND_SPEED, ATTR_FORECAST_NATIVE_WIND_SPEED, default=0),
|
||||
AttributeMapper(
|
||||
ATTR_API_FEELS_LIKE_TEMPERATURE, ATTR_FORECAST_NATIVE_APPARENT_TEMP
|
||||
),
|
||||
AttributeMapper(ATTR_API_TEMPERATURE, ATTR_FORECAST_NATIVE_TEMP),
|
||||
AttributeMapper(
|
||||
ATTR_API_CONDITION, ATTR_FORECAST_CONDITION, mapping=WEATHER_STATES_CONVERSION
|
||||
),
|
||||
AttributeMapper(ATTR_API_WIND_GUST, ATTR_FORECAST_NATIVE_WIND_GUST_SPEED),
|
||||
]
|
||||
|
||||
|
||||
FORECAST_DAY_ATTRIBUTE_TRANSLATION: list[AttributeMapper] = [
|
||||
AttributeMapper(ATTR_API_WIND_BEARING, ATTR_FORECAST_WIND_BEARING),
|
||||
AttributeMapper(ATTR_API_WIND_SPEED, ATTR_FORECAST_NATIVE_WIND_SPEED, default=0),
|
||||
AttributeMapper(
|
||||
ATTR_API_FEELS_LIKE_TEMPERATURE, ATTR_FORECAST_NATIVE_APPARENT_TEMP
|
||||
),
|
||||
AttributeMapper(ATTR_API_TEMPERATURE_MAX, ATTR_FORECAST_NATIVE_TEMP),
|
||||
AttributeMapper(ATTR_API_TEMPERATURE_MIN, ATTR_FORECAST_NATIVE_TEMP_LOW),
|
||||
AttributeMapper(
|
||||
ATTR_API_CONDITION, ATTR_FORECAST_CONDITION, mapping=WEATHER_STATES_CONVERSION
|
||||
),
|
||||
AttributeMapper(ATTR_API_WIND_GUST, ATTR_FORECAST_NATIVE_WIND_GUST_SPEED),
|
||||
]
|
||||
|
||||
|
||||
class WeatherUpdater(DataUpdateCoordinator):
|
||||
"""Weather data updater for interaction with Yandex.Weather API."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
latitude: float,
|
||||
longitude: float,
|
||||
api_key: str,
|
||||
hass: HomeAssistant,
|
||||
device_id: str,
|
||||
name="Yandex Weather",
|
||||
updates_per_day: int = UPDATES_PER_DAY_MAP[DEFAULT_UPDATES_PER_DAY],
|
||||
weather_data: dict | None = None,
|
||||
):
|
||||
"""Initialize updater.
|
||||
|
||||
:param latitude: latitude of location for weather data
|
||||
:param longitude: longitude of location for weather data
|
||||
:param api_key: Yandex weather API. MUST be weather for site tariff plan
|
||||
:param hass: Home Assistant object
|
||||
:param language: Language for yandex_condition
|
||||
:param updates_per_day: int: how many updates per day we should do?
|
||||
:param device_id: ID of integration Device in Home Assistant
|
||||
:param weather_data: if not None then weather_data will be used for first update
|
||||
"""
|
||||
|
||||
self.__api_key = api_key.strip()
|
||||
self._lat = latitude
|
||||
self._lon = longitude
|
||||
self._device_id = device_id
|
||||
self._name = name
|
||||
self.weather_data = weather_data
|
||||
self.is_first_update_requred = bool(weather_data)
|
||||
|
||||
self.update_interval = timedelta(
|
||||
seconds=math.ceil((24 * 60 * 60) / updates_per_day)
|
||||
)
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=f"{self._name} updater",
|
||||
update_interval=self.update_interval,
|
||||
update_method=self.update,
|
||||
)
|
||||
self.data = {}
|
||||
|
||||
def process_data(
|
||||
self, dst: dict, src: dict, attributes: list[AttributeMapper], is_day: bool
|
||||
):
|
||||
"""Convert Yandex API weather response to HA friendly.
|
||||
|
||||
:param dst: weather data for HomeAssistant
|
||||
:param src: weather data form Yandex
|
||||
:param attributes: how to translate src to dst
|
||||
"""
|
||||
|
||||
for attribute in attributes:
|
||||
value = src.get(attribute.src, attribute.default)
|
||||
|
||||
if value is not None and attribute.mapping is not None:
|
||||
if isinstance(attribute.mapping, dict):
|
||||
value = map_state(
|
||||
src=str(value), is_day=is_day, mapping=attribute.mapping
|
||||
)
|
||||
else:
|
||||
value = attribute.mapping(value)
|
||||
|
||||
dst[attribute.dst] = value
|
||||
|
||||
@staticmethod
|
||||
def get_min_forecast_temperature(forecasts: list[dict]) -> float | None:
|
||||
"""Get minimum temperature from forecast data."""
|
||||
low_fc_temperatures: list[float] = []
|
||||
|
||||
for f in forecasts:
|
||||
f_low_temperature: float = f.get(ATTR_FORECAST_NATIVE_TEMP, None)
|
||||
if f_low_temperature is not None:
|
||||
low_fc_temperatures.append(f_low_temperature)
|
||||
|
||||
return min(low_fc_temperatures) if len(low_fc_temperatures) > 0 else None
|
||||
|
||||
async def _get_weather_data(self):
|
||||
if self.is_first_update_requred: # No need to make API request
|
||||
self.is_first_update_requred = False
|
||||
_LOGGER.info("Skip API request")
|
||||
return self.weather_data
|
||||
|
||||
timeout = aiohttp.ClientTimeout(total=20)
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
rs = await self.request(session, self.__api_key, self._lat, self._lon)
|
||||
|
||||
self.weather_data = rs["data"]
|
||||
return self.weather_data
|
||||
|
||||
async def update(self):
|
||||
"""Update weather information.
|
||||
|
||||
:returns: dict with weather data.
|
||||
"""
|
||||
data = await self._get_weather_data()
|
||||
weather_by_point = data["weatherByPoint"]
|
||||
now = weather_by_point["now"]
|
||||
now_dt = parser.parse(data[ATTR_API_SERVER_TIME])
|
||||
result = {
|
||||
ATTR_API_SERVER_TIME: now_dt,
|
||||
ATTR_FORECAST_DATA: {
|
||||
ATTR_FORECAST_HOURLY: [],
|
||||
ATTR_FORECAST_TWICE_DAILY: [],
|
||||
},
|
||||
ATTR_FORECAST_HOURLY_ICONS: [],
|
||||
ATTR_FORECAST_TWICE_DAILY_ICONS: [],
|
||||
ATTR_API_SUNRISE_BEGIN_TIME: "",
|
||||
ATTR_API_SUNRISE_END_TIME: ""
|
||||
}
|
||||
self.process_data(
|
||||
result,
|
||||
now,
|
||||
CURRENT_WEATHER_ATTRIBUTE_TRANSLATION,
|
||||
now[ATTR_API_DAYTIME] == "DAY",
|
||||
)
|
||||
|
||||
for day in weather_by_point["forecast"]["days"]:
|
||||
sunrise_begin = parser.parse(day[ATTR_API_SUNRISE_BEGIN_TIME])
|
||||
sunset_end = parser.parse(day[ATTR_API_SUNRISE_END_TIME])
|
||||
day_dt = parser.parse(day[ATTR_API_TIME])
|
||||
polar = day.get(ATTR_API_POLAR)
|
||||
|
||||
if sunrise_begin > now_dt and not result[ATTR_API_SUNRISE_BEGIN_TIME]:
|
||||
result[ATTR_API_SUNRISE_BEGIN_TIME] = sunrise_begin
|
||||
|
||||
if sunset_end > now_dt and not result[ATTR_API_SUNRISE_END_TIME]:
|
||||
result[ATTR_API_SUNRISE_END_TIME] = sunset_end
|
||||
|
||||
for hour in day["hours"]:
|
||||
hour_dt = parser.parse(hour[ATTR_API_TIME])
|
||||
if now_dt > hour_dt:
|
||||
continue
|
||||
|
||||
hour_forecast = Forecast(datetime=hour_dt)
|
||||
self.process_data(
|
||||
dst=hour_forecast,
|
||||
src=hour,
|
||||
attributes=FORECAST_HOUR_ATTRIBUTE_TRANSLATION,
|
||||
is_day=is_daytime(
|
||||
hour_dt, day_dt, sunrise_begin, sunset_end, polar
|
||||
),
|
||||
)
|
||||
result[ATTR_FORECAST_DATA][ATTR_FORECAST_HOURLY].append(hour_forecast)
|
||||
result[ATTR_FORECAST_HOURLY_ICONS].append(hour.get(ATTR_API_IMAGE))
|
||||
|
||||
day_part = day["summary"]["day"]
|
||||
night_part = day["summary"]["night"]
|
||||
for part, is_day in ((night_part, False), (day_part, True)):
|
||||
part_forecast = Forecast(datetime=day_dt, is_daytime=is_day)
|
||||
self.process_data(
|
||||
dst=part_forecast,
|
||||
src=part,
|
||||
attributes=FORECAST_DAY_ATTRIBUTE_TRANSLATION,
|
||||
is_day=is_day,
|
||||
)
|
||||
result[ATTR_FORECAST_DATA][ATTR_FORECAST_TWICE_DAILY].append(
|
||||
part_forecast
|
||||
)
|
||||
result[ATTR_FORECAST_TWICE_DAILY_ICONS].append(part.get(ATTR_API_IMAGE))
|
||||
day_dt = day_dt + timedelta(hours=12)
|
||||
|
||||
result[ATTR_MIN_FORECAST_TEMPERATURE] = self.get_min_forecast_temperature(
|
||||
result[ATTR_FORECAST_DATA][ATTR_FORECAST_HOURLY]
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
async def request(
|
||||
session: aiohttp.ClientSession,
|
||||
api_key: str,
|
||||
lat: float,
|
||||
lon: float,
|
||||
):
|
||||
"""Make request to API endpoint.
|
||||
|
||||
:param session: aiohttp.ClientSession: HTTP session for request
|
||||
:param api_key: str: API key
|
||||
:param lat: float: latitude of location where we are getting weather data
|
||||
:param lon: float: longitude of location where we ate getting weather data
|
||||
|
||||
:returns: dict with response data
|
||||
:raises AssertionError: when response.status is not 200
|
||||
"""
|
||||
_LOGGER.info("Sending API request")
|
||||
async with session.post(
|
||||
API_URL,
|
||||
headers={API_HEADER_NAME: api_key},
|
||||
json={"query": API_QUERY_TEMPLATE % (lat, lon)},
|
||||
) as response:
|
||||
try:
|
||||
assert response.status == 200
|
||||
except AssertionError as e:
|
||||
_LOGGER.error("Could not get data from API: %s", response)
|
||||
raise aiohttp.ClientError(response.status, await response.text()) from e
|
||||
|
||||
result = await response.json()
|
||||
|
||||
if "errors" in result:
|
||||
msg = f"Errors in weather API response: {result}"
|
||||
_LOGGER.error(msg)
|
||||
raise HomeAssistantError(msg)
|
||||
|
||||
_LOGGER.debug(result)
|
||||
return result
|
||||
|
||||
def __str__(self):
|
||||
"""Show as pretty look data json."""
|
||||
_d = dict(self.data)
|
||||
_d[ATTR_API_SERVER_TIME] = str(_d[ATTR_API_SERVER_TIME])
|
||||
return json.dumps(_d, indent=4, sort_keys=True)
|
||||
|
||||
@property
|
||||
def url(self) -> str:
|
||||
"""Weather URL."""
|
||||
return f"https://yandex.ru/weather/?lat={self._lat}&lon={self._lon}"
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Device info."""
|
||||
return DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, self.device_id)},
|
||||
manufacturer=MANUFACTURER,
|
||||
name=self._name,
|
||||
configuration_url=self.url,
|
||||
)
|
||||
|
||||
def schedule_refresh(self, offset: timedelta):
|
||||
"""Schedule refresh."""
|
||||
if self._unsub_refresh:
|
||||
self._unsub_refresh()
|
||||
self._unsub_refresh = None
|
||||
|
||||
_LOGGER.debug(f"scheduling next refresh after {offset=}")
|
||||
next_refresh = (
|
||||
int(self.hass.loop.time()) + self._microsecond + offset.total_seconds()
|
||||
)
|
||||
self._unsub_refresh = self.hass.loop.call_at(
|
||||
next_refresh, self.__wrap_handle_refresh_interval
|
||||
).cancel
|
||||
|
||||
@callback
|
||||
def __wrap_handle_refresh_interval(self) -> None:
|
||||
"""Handle a refresh interval occurrence."""
|
||||
# We need this private callback from parent class
|
||||
if self.config_entry:
|
||||
self.config_entry.async_create_background_task(
|
||||
self.hass,
|
||||
self._handle_refresh_interval(),
|
||||
name=f"{self.name} - {self.config_entry.title} - refresh",
|
||||
eager_start=True,
|
||||
)
|
||||
else:
|
||||
self.hass.async_create_background_task(
|
||||
self._handle_refresh_interval(),
|
||||
name=f"{self.name} - refresh",
|
||||
eager_start=True,
|
||||
)
|
||||
|
||||
@property
|
||||
def device_id(self) -> str:
|
||||
"""Device ID."""
|
||||
return self._device_id
|
||||
258
homeassistant/config/custom_components/yandex_pogoda/weather.py
Normal file
258
homeassistant/config/custom_components/yandex_pogoda/weather.py
Normal file
@@ -0,0 +1,258 @@
|
||||
"""Pogoda component."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime, timezone
|
||||
import logging
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
Forecast,
|
||||
WeatherEntity,
|
||||
WeatherEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
UnitOfSpeed,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import (
|
||||
ATTR_API_CONDITION,
|
||||
ATTR_API_FEELS_LIKE_TEMPERATURE,
|
||||
ATTR_API_IMAGE,
|
||||
ATTR_API_TEMPERATURE,
|
||||
ATTR_API_WIND_BEARING,
|
||||
ATTR_API_WIND_GUST,
|
||||
ATTR_API_WIND_SPEED,
|
||||
ATTR_API_YA_CONDITION,
|
||||
ATTR_FORECAST_DATA,
|
||||
ATTR_FORECAST_HOURLY,
|
||||
ATTR_FORECAST_HOURLY_ICONS,
|
||||
ATTR_FORECAST_TWICE_DAILY,
|
||||
ATTR_FORECAST_TWICE_DAILY_ICONS,
|
||||
ATTRIBUTION,
|
||||
DOMAIN,
|
||||
ENTRY_NAME,
|
||||
TEMPERATURE_CONVERTER,
|
||||
UPDATER,
|
||||
WIND_SPEED_CONVERTER,
|
||||
convert_unit_value,
|
||||
)
|
||||
from .device_trigger import TRIGGERS
|
||||
from .updater import WeatherUpdater
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_converter(
|
||||
converter: Callable[[float, str, str], float], unit_from: str, unit_to: str
|
||||
) -> Callable[[float], float | None]:
|
||||
def wrap(value: float | None) -> float | None:
|
||||
return convert_unit_value(converter, value, unit_from, unit_to)
|
||||
|
||||
return wrap
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up weather "Yandex.Pogoda" weather entry."""
|
||||
domain_data = hass.data[DOMAIN][config_entry.entry_id]
|
||||
name = domain_data[ENTRY_NAME]
|
||||
updater = domain_data[UPDATER]
|
||||
|
||||
async_add_entities([YandexWeather(name, config_entry, updater, hass)], False)
|
||||
|
||||
|
||||
class YandexWeather(WeatherEntity, CoordinatorEntity, RestoreEntity):
|
||||
"""Yandex.Pogoda entry."""
|
||||
|
||||
_attr_attribution = ATTRIBUTION
|
||||
_attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND
|
||||
_attr_native_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_hourly_forecast: list[Forecast] | None
|
||||
_twice_daily_forecast: list[Forecast] | None
|
||||
_attr_supported_features = (
|
||||
WeatherEntityFeature.FORECAST_HOURLY | WeatherEntityFeature.FORECAST_TWICE_DAILY
|
||||
)
|
||||
_unrecorded_attributes = frozenset(
|
||||
{
|
||||
"forecast_hourly",
|
||||
"forecast_twice_daily",
|
||||
ATTR_FORECAST_HOURLY_ICONS,
|
||||
ATTR_FORECAST_TWICE_DAILY_ICONS,
|
||||
}
|
||||
)
|
||||
coordinator: WeatherUpdater
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name,
|
||||
config_entry: ConfigEntry,
|
||||
updater: WeatherUpdater,
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Initialize entry."""
|
||||
WeatherEntity.__init__(self)
|
||||
CoordinatorEntity.__init__(self, coordinator=updater)
|
||||
RestoreEntity.__init__(self)
|
||||
|
||||
self.hass = hass
|
||||
self._attr_name = name
|
||||
self._attr_condition = None
|
||||
self._attr_unique_id = config_entry.unique_id
|
||||
self._hourly_forecast = []
|
||||
self._twice_daily_forecast = []
|
||||
self._attr_device_info = self.coordinator.device_info
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to hass."""
|
||||
await RestoreEntity.async_added_to_hass(self)
|
||||
await CoordinatorEntity.async_added_to_hass(self)
|
||||
|
||||
state = await self.async_get_last_state()
|
||||
if not state:
|
||||
_LOGGER.debug("Have no state for restore!")
|
||||
await self.coordinator.async_config_entry_first_refresh()
|
||||
return
|
||||
|
||||
if state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
|
||||
self._attr_available = False
|
||||
await self.coordinator.async_config_entry_first_refresh()
|
||||
else:
|
||||
_LOGGER.debug(f"state for restore: {state}")
|
||||
self._attr_available = True
|
||||
self._attr_condition = state.state
|
||||
temp_converter = _get_converter(
|
||||
TEMPERATURE_CONVERTER,
|
||||
state.attributes.get("temperature_unit")
|
||||
or self._attr_native_temperature_unit,
|
||||
self._attr_native_temperature_unit,
|
||||
)
|
||||
ws_converter = _get_converter(
|
||||
WIND_SPEED_CONVERTER,
|
||||
state.attributes.get("wind_speed_unit")
|
||||
or self._attr_native_wind_speed_unit,
|
||||
self._attr_native_wind_speed_unit,
|
||||
)
|
||||
|
||||
self._attr_native_temperature = temp_converter(
|
||||
state.attributes.get("temperature")
|
||||
)
|
||||
self._attr_native_wind_speed = ws_converter(
|
||||
state.attributes.get("wind_speed")
|
||||
)
|
||||
self._attr_native_wind_gust_speed = ws_converter(
|
||||
state.attributes.get("wind_gust_speed")
|
||||
)
|
||||
self._attr_native_apparent_temperature = temp_converter(
|
||||
state.attributes.get("apparent_temperature")
|
||||
)
|
||||
self._attr_wind_bearing = state.attributes.get("wind_bearing")
|
||||
self._attr_entity_picture = state.attributes.get("entity_picture")
|
||||
|
||||
self._attr_extra_state_attributes = {}
|
||||
|
||||
f_data = {
|
||||
ATTR_FORECAST_HOURLY: state.attributes.get("forecast_hourly"),
|
||||
ATTR_FORECAST_TWICE_DAILY: state.attributes.get("forecast_twice_daily"),
|
||||
}
|
||||
self._update_forecast_data(f_data)
|
||||
for attr in (
|
||||
ATTR_API_YA_CONDITION,
|
||||
ATTR_FORECAST_HOURLY_ICONS,
|
||||
ATTR_FORECAST_TWICE_DAILY_ICONS,
|
||||
):
|
||||
if value := state.attributes.get(attr):
|
||||
self._attr_extra_state_attributes[attr] = value
|
||||
|
||||
# last_updated is last call of self.async_write_ha_state(), not a real last update
|
||||
since_last_update = datetime.now(timezone.utc) - state.last_updated.replace(
|
||||
tzinfo=timezone.utc
|
||||
)
|
||||
_LOGGER.debug(
|
||||
f"Time since last update: {since_last_update} ({state.last_updated}), "
|
||||
f"update interval is {self.coordinator.update_interval}"
|
||||
)
|
||||
if since_last_update > self.coordinator.update_interval:
|
||||
await self.coordinator.async_config_entry_first_refresh()
|
||||
else:
|
||||
self.coordinator.schedule_refresh(
|
||||
offset=self.coordinator.update_interval - since_last_update
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
|
||||
def _update_forecast_data(self, forecast_data: dict):
|
||||
self._hourly_forecast = forecast_data.get(ATTR_FORECAST_HOURLY, [])
|
||||
self._twice_daily_forecast = forecast_data.get(ATTR_FORECAST_TWICE_DAILY, [])
|
||||
self._attr_extra_state_attributes["forecast_hourly"] = self._hourly_forecast
|
||||
self._attr_extra_state_attributes["forecast_twice_daily"] = (
|
||||
self._twice_daily_forecast
|
||||
)
|
||||
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
self._attr_available = True
|
||||
self.update_condition_and_fire_event(
|
||||
new_condition=self.coordinator.data.get(ATTR_API_CONDITION)
|
||||
)
|
||||
self._attr_entity_picture = self.coordinator.data.get(ATTR_API_IMAGE)
|
||||
self._attr_native_temperature = self.coordinator.data.get(ATTR_API_TEMPERATURE)
|
||||
self._attr_native_wind_speed = self.coordinator.data.get(ATTR_API_WIND_SPEED)
|
||||
self._attr_wind_bearing = self.coordinator.data.get(ATTR_API_WIND_BEARING)
|
||||
self._attr_native_wind_gust_speed = self.coordinator.data.get(
|
||||
ATTR_API_WIND_GUST
|
||||
)
|
||||
self._attr_native_apparent_temperature = self.coordinator.data.get(
|
||||
ATTR_API_FEELS_LIKE_TEMPERATURE
|
||||
)
|
||||
_LOGGER.debug(f"_handle_coordinator_update: {self._hourly_forecast=}")
|
||||
self._attr_extra_state_attributes = {
|
||||
ATTR_API_YA_CONDITION: self.coordinator.data.get(ATTR_API_YA_CONDITION),
|
||||
ATTR_FORECAST_HOURLY_ICONS: self.coordinator.data.get(
|
||||
ATTR_FORECAST_HOURLY_ICONS
|
||||
),
|
||||
ATTR_FORECAST_TWICE_DAILY_ICONS: self.coordinator.data.get(
|
||||
ATTR_FORECAST_TWICE_DAILY_ICONS
|
||||
),
|
||||
}
|
||||
self._update_forecast_data(self.coordinator.data.get(ATTR_FORECAST_DATA, {}))
|
||||
self.async_write_ha_state()
|
||||
|
||||
def update_condition_and_fire_event(self, new_condition: str):
|
||||
"""Set new condition and fire event on change."""
|
||||
if (
|
||||
new_condition != self._attr_condition
|
||||
and self.hass is not None
|
||||
and new_condition in TRIGGERS
|
||||
):
|
||||
self.hass.bus.async_fire(
|
||||
DOMAIN + "_event",
|
||||
{
|
||||
"device_id": self.coordinator.device_id,
|
||||
"type": new_condition,
|
||||
},
|
||||
)
|
||||
|
||||
self._attr_condition = new_condition
|
||||
|
||||
async def async_forecast_hourly(self) -> list[Forecast] | None:
|
||||
"""Return the hourly forecast in native units.
|
||||
|
||||
Only implement this method if `WeatherEntityFeature.FORECAST_HOURLY` is set
|
||||
"""
|
||||
_LOGGER.debug(f"async_forecast_hourly: {self._hourly_forecast=}")
|
||||
return self._hourly_forecast if self._hourly_forecast else []
|
||||
|
||||
async def async_forecast_twice_daily(self) -> list[Forecast] | None:
|
||||
"""Return the daily forecast in native units."""
|
||||
_LOGGER.debug(f"async_forecast_twice_daily: {self._twice_daily_forecast=}")
|
||||
return self._twice_daily_forecast if self._twice_daily_forecast else []
|
||||
Reference in New Issue
Block a user