Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@ homeassistant/components/tile/* @bachya
homeassistant/components/time_date/* @fabaff
homeassistant/components/tmb/* @alemuro
homeassistant/components/todoist/* @boralyl
homeassistant/components/tomorrowio/* @raman325
homeassistant/components/toon/* @frenck
homeassistant/components/totalconnect/* @austinmroczek
homeassistant/components/tplink/* @rytilahti @thegardenmonkey
Expand Down
231 changes: 231 additions & 0 deletions homeassistant/components/tomorrowio/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
"""The Tomorrow.io integration."""
from __future__ import annotations

from datetime import timedelta
import logging
from math import ceil
from typing import Any

from pytomorrowio import TomorrowioV4
from pytomorrowio.const import CURRENT, FORECASTS
from pytomorrowio.exceptions import (
CantConnectException,
InvalidAPIKeyException,
RateLimitedException,
UnknownException,
)

from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN
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 homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
UpdateFailed,
)

from .const import (
ATTRIBUTION,
CC_ATTR_CLOUD_COVER,
CC_ATTR_CONDITION,
CC_ATTR_HUMIDITY,
CC_ATTR_OZONE,
CC_ATTR_PRECIPITATION,
CC_ATTR_PRECIPITATION_PROBABILITY,
CC_ATTR_PRECIPITATION_TYPE,
CC_ATTR_PRESSURE,
CC_ATTR_TEMPERATURE,
CC_ATTR_TEMPERATURE_HIGH,
CC_ATTR_TEMPERATURE_LOW,
CC_ATTR_VISIBILITY,
CC_ATTR_WIND_DIRECTION,
CC_ATTR_WIND_GUST,
CC_ATTR_WIND_SPEED,
CC_SENSOR_TYPES,
CONF_TIMESTEP,
DEFAULT_TIMESTEP,
DOMAIN,
MAX_REQUESTS_PER_DAY,
)

_LOGGER = logging.getLogger(__name__)

PLATFORMS = [SENSOR_DOMAIN, WEATHER_DOMAIN]


def _set_update_interval(hass: HomeAssistant, current_entry: ConfigEntry) -> timedelta:
"""Recalculate update_interval based on existing Tomorrow.io instances and update them."""
api_calls = 2
# We check how many Tomorrow.io configured instances are using the same API key and
# calculate interval to not exceed allowed numbers of requests. Divide 90% of
# MAX_REQUESTS_PER_DAY by the number of API calls because we want a buffer in the
# number of API calls left at the end of the day.
other_instance_entry_ids = [
entry.entry_id
for entry in hass.config_entries.async_entries(DOMAIN)
if entry.entry_id != current_entry.entry_id
and entry.data[CONF_API_KEY] == current_entry.data[CONF_API_KEY]
]

interval = timedelta(
minutes=(
ceil(
(24 * 60 * (len(other_instance_entry_ids) + 1) * api_calls)
/ (MAX_REQUESTS_PER_DAY * 0.9)
)
)
)

for entry_id in other_instance_entry_ids:
if entry_id in hass.data[DOMAIN]:
hass.data[DOMAIN][entry_id].update_interval = interval

return interval


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Tomorrow.io API from a config entry."""
hass.data.setdefault(DOMAIN, {})

# If config entry options not set up, set them up
Comment thread
raman325 marked this conversation as resolved.
Outdated
if not entry.options:
hass.config_entries.async_update_entry(
entry,
options={
CONF_TIMESTEP: DEFAULT_TIMESTEP,
},
)

api = TomorrowioV4(
entry.data[CONF_API_KEY],
entry.data[CONF_LATITUDE],
entry.data[CONF_LONGITUDE],
session=async_get_clientsession(hass),
)

coordinator = TomorrowioDataUpdateCoordinator(
hass,
entry,
api,
_set_update_interval(hass, entry),
)

await coordinator.async_config_entry_first_refresh()

hass.data[DOMAIN][entry.entry_id] = coordinator

hass.config_entries.async_setup_platforms(entry, PLATFORMS)

return True


async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS
)

hass.data[DOMAIN].pop(config_entry.entry_id)
if not hass.data[DOMAIN]:
hass.data.pop(DOMAIN)

return unload_ok


class TomorrowioDataUpdateCoordinator(DataUpdateCoordinator):
"""Define an object to hold Tomorrow.io data."""

def __init__(
self,
hass: HomeAssistant,
config_entry: ConfigEntry,
api: TomorrowioV4,
update_interval: timedelta,
) -> None:
"""Initialize."""

self._config_entry = config_entry
self._api = api
self.name = config_entry.data[CONF_NAME]
self.data = {CURRENT: {}, FORECASTS: {}}

super().__init__(
hass,
_LOGGER,
name=config_entry.data[CONF_NAME],
update_interval=update_interval,
)

async def _async_update_data(self) -> dict[str, Any]:
"""Update data via library."""
try:
return await self._api.realtime_and_all_forecasts(
[
CC_ATTR_TEMPERATURE,
CC_ATTR_HUMIDITY,
CC_ATTR_PRESSURE,
CC_ATTR_WIND_SPEED,
CC_ATTR_WIND_DIRECTION,
CC_ATTR_CONDITION,
CC_ATTR_VISIBILITY,
CC_ATTR_OZONE,
CC_ATTR_WIND_GUST,
CC_ATTR_CLOUD_COVER,
CC_ATTR_PRECIPITATION_TYPE,
*(sensor_type.key for sensor_type in CC_SENSOR_TYPES),
],
[
CC_ATTR_TEMPERATURE_LOW,
CC_ATTR_TEMPERATURE_HIGH,
CC_ATTR_WIND_SPEED,
CC_ATTR_WIND_DIRECTION,
CC_ATTR_CONDITION,
CC_ATTR_PRECIPITATION,
CC_ATTR_PRECIPITATION_PROBABILITY,
],
)
except (
CantConnectException,
InvalidAPIKeyException,
RateLimitedException,
UnknownException,
) as error:
raise UpdateFailed from error


class TomorrowioEntity(CoordinatorEntity):
"""Base Tomorrow.io Entity."""

def __init__(
self,
config_entry: ConfigEntry,
coordinator: TomorrowioDataUpdateCoordinator,
api_version: int,
) -> None:
"""Initialize Tomorrow.io Entity."""
super().__init__(coordinator)
self.api_version = api_version
self._config_entry = config_entry
self._attr_device_info = {
"identifiers": {(DOMAIN, self._config_entry.data[CONF_API_KEY])},
"name": "Tomorrow.io",
"manufacturer": "Tomorrow.io",
"sw_version": f"v{self.api_version}",
"entry_type": "service",
}

def _get_current_property(self, property_name: str) -> int | str | float | None:
"""
Get property from current conditions.

Used for V4 API.
"""
return self.coordinator.data.get(CURRENT, {}).get(property_name)

@property
def attribution(self):
"""Return the attribution."""
return ATTRIBUTION
154 changes: 154 additions & 0 deletions homeassistant/components/tomorrowio/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"""Config flow for Tomorrow.io integration."""
from __future__ import annotations

import logging
from typing import Any

from pytomorrowio.exceptions import (
CantConnectException,
InvalidAPIKeyException,
RateLimitedException,
)
from pytomorrowio.pytomorrowio import TomorrowioV4
import voluptuous as vol

from homeassistant import config_entries, core
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv

from .const import (
CC_ATTR_TEMPERATURE,
CONF_TIMESTEP,
DEFAULT_NAME,
DEFAULT_TIMESTEP,
DOMAIN,
)

_LOGGER = logging.getLogger(__name__)


def _get_config_schema(
hass: core.HomeAssistant, source: str | None, input_dict: dict[str, Any] = None
) -> vol.Schema:
"""
Return schema defaults for init step based on user input/config dict.

Retain info already provided for future form views by setting them as
defaults in schema.
"""
if input_dict is None:
input_dict = {}

api_key_schema = {
vol.Required(CONF_API_KEY, default=input_dict.get(CONF_API_KEY)): str,
}

if source == config_entries.SOURCE_USER:
return vol.Schema(
{
vol.Required(
CONF_NAME, default=input_dict.get(CONF_NAME, DEFAULT_NAME)
Comment thread
raman325 marked this conversation as resolved.
Outdated
): str,
**api_key_schema,
vol.Required(
CONF_LATITUDE,
"location",
default=input_dict.get(CONF_LATITUDE, hass.config.latitude),
): cv.latitude,
vol.Required(
CONF_LONGITUDE,
"location",
default=input_dict.get(CONF_LONGITUDE, hass.config.longitude),
): cv.longitude,
},
extra=vol.REMOVE_EXTRA,
)

# For imports we just need to ask for the API key
return vol.Schema(api_key_schema, extra=vol.REMOVE_EXTRA)


def _get_unique_id(hass: HomeAssistant, input_dict: dict[str, Any]):
"""Return unique ID from config data."""
return (
f"{input_dict[CONF_API_KEY]}"
f"_{input_dict.get(CONF_LATITUDE, hass.config.latitude)}"
f"_{input_dict.get(CONF_LONGITUDE, hass.config.longitude)}"
)


class TomorrowioOptionsConfigFlow(config_entries.OptionsFlow):
"""Handle Tomorrow.io options."""

def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize Tomorrow.io options flow."""
self._config_entry = config_entry

async def async_step_init(self, user_input: dict[str, Any] = None) -> FlowResult:
"""Manage the Tomorrow.io options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)

options_schema = {
vol.Required(
CONF_TIMESTEP,
Comment thread
raman325 marked this conversation as resolved.
default=self._config_entry.options.get(CONF_TIMESTEP, DEFAULT_TIMESTEP),
): vol.In([1, 5, 15, 30]),
}

return self.async_show_form(
step_id="init", data_schema=vol.Schema(options_schema)
)


class TomorrowioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Tomorrow.io Weather API."""

VERSION = 1

@staticmethod
@callback
def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> TomorrowioOptionsConfigFlow:
"""Get the options flow for this handler."""
return TomorrowioOptionsConfigFlow(config_entry)

async def async_step_user(self, user_input: dict[str, Any] = None) -> FlowResult:
"""Handle the initial step."""
errors = {}
if user_input is not None:
await self.async_set_unique_id(
unique_id=_get_unique_id(self.hass, user_input)
)
self._abort_if_unique_id_configured()

try:
await TomorrowioV4(
user_input[CONF_API_KEY],
str(user_input.get(CONF_LATITUDE, self.hass.config.latitude)),
str(user_input.get(CONF_LONGITUDE, self.hass.config.longitude)),
session=async_get_clientsession(self.hass),
).realtime([CC_ATTR_TEMPERATURE])

return self.async_create_entry(
Comment thread
raman325 marked this conversation as resolved.
Outdated
title=user_input[CONF_NAME], data=user_input
)
except CantConnectException:
errors["base"] = "cannot_connect"
except InvalidAPIKeyException:
errors[CONF_API_KEY] = "invalid_api_key"
except RateLimitedException:
errors[CONF_API_KEY] = "rate_limited"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"

return self.async_show_form(
step_id="user",
data_schema=_get_config_schema(self.hass, self.source, user_input),
errors=errors,
)
Loading