Add Open Exchange Rates coordinator (#76017)

* Add Open Exchange Rates coordinator

* Move debug log

* Fix update interval calculation
This commit is contained in:
Martin Hjelmare 2022-08-02 14:49:46 +02:00 committed by GitHub
parent 404d530b5f
commit cfe6c8939c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 133 additions and 69 deletions

View File

@ -850,7 +850,7 @@ omit =
homeassistant/components/open_meteo/weather.py
homeassistant/components/opencv/*
homeassistant/components/openevse/sensor.py
homeassistant/components/openexchangerates/sensor.py
homeassistant/components/openexchangerates/*
homeassistant/components/opengarage/__init__.py
homeassistant/components/opengarage/binary_sensor.py
homeassistant/components/opengarage/cover.py

View File

@ -766,6 +766,7 @@ build.json @home-assistant/supervisor
/tests/components/open_meteo/ @frenck
/homeassistant/components/openerz/ @misialq
/tests/components/openerz/ @misialq
/homeassistant/components/openexchangerates/ @MartinHjelmare
/homeassistant/components/opengarage/ @danielhiversen
/tests/components/opengarage/ @danielhiversen
/homeassistant/components/openhome/ @bazwilliams

View File

@ -0,0 +1,7 @@
"""Provide common constants for Open Exchange Rates."""
from datetime import timedelta
import logging
DOMAIN = "openexchangerates"
LOGGER = logging.getLogger(__package__)
BASE_UPDATE_INTERVAL = timedelta(hours=2)

View File

@ -0,0 +1,47 @@
"""Provide an OpenExchangeRates data coordinator."""
from __future__ import annotations
import asyncio
from datetime import timedelta
from aiohttp import ClientSession
from aioopenexchangerates import Client, Latest, OpenExchangeRatesClientError
import async_timeout
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, LOGGER
TIMEOUT = 10
class OpenexchangeratesCoordinator(DataUpdateCoordinator[Latest]):
"""Represent a coordinator for Open Exchange Rates API."""
def __init__(
self,
hass: HomeAssistant,
session: ClientSession,
api_key: str,
base: str,
update_interval: timedelta,
) -> None:
"""Initialize the coordinator."""
super().__init__(
hass, LOGGER, name=f"{DOMAIN} base {base}", update_interval=update_interval
)
self.base = base
self.client = Client(api_key, session)
self.setup_lock = asyncio.Lock()
async def _async_update_data(self) -> Latest:
"""Update data from Open Exchange Rates."""
try:
async with async_timeout.timeout(TIMEOUT):
latest = await self.client.get_latest(base=self.base)
except (OpenExchangeRatesClientError) as err:
raise UpdateFailed(err) from err
LOGGER.debug("Result: %s", latest)
return latest

View File

@ -2,6 +2,7 @@
"domain": "openexchangerates",
"name": "Open Exchange Rates",
"documentation": "https://www.home-assistant.io/integrations/openexchangerates",
"codeowners": [],
"requirements": ["aioopenexchangerates==0.3.0"],
"codeowners": ["@MartinHjelmare"],
"iot_class": "cloud_polling"
}

View File

@ -1,32 +1,28 @@
"""Support for openexchangerates.org exchange rates service."""
from __future__ import annotations
from datetime import timedelta
from http import HTTPStatus
import logging
from typing import Any
from dataclasses import dataclass, field
import requests
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_API_KEY, CONF_BASE, CONF_NAME, CONF_QUOTE
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import Throttle
from homeassistant.helpers.update_coordinator import CoordinatorEntity
_LOGGER = logging.getLogger(__name__)
_RESOURCE = "https://openexchangerates.org/api/latest.json"
from .const import BASE_UPDATE_INTERVAL, DOMAIN, LOGGER
from .coordinator import OpenexchangeratesCoordinator
ATTRIBUTION = "Data provided by openexchangerates.org"
DEFAULT_BASE = "USD"
DEFAULT_NAME = "Exchange Rate Sensor"
MIN_TIME_BETWEEN_UPDATES = timedelta(hours=2)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_API_KEY): cv.string,
@ -37,10 +33,19 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
)
def setup_platform(
@dataclass
class DomainData:
"""Data structure to hold data for this domain."""
coordinators: dict[tuple[str, str], OpenexchangeratesCoordinator] = field(
default_factory=dict, init=False
)
async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Open Exchange Rates sensor."""
@ -49,75 +54,75 @@ def setup_platform(
base: str = config[CONF_BASE]
quote: str = config[CONF_QUOTE]
parameters = {"base": base, "app_id": api_key}
integration_data: DomainData = hass.data.setdefault(DOMAIN, DomainData())
coordinators = integration_data.coordinators
rest = OpenexchangeratesData(_RESOURCE, parameters, quote)
response = requests.get(_RESOURCE, params=parameters, timeout=10)
if (api_key, base) not in coordinators:
# Create one coordinator per base currency per API key.
update_interval = BASE_UPDATE_INTERVAL * (
len(
{
coordinator_base
for coordinator_api_key, coordinator_base in coordinators
if coordinator_api_key == api_key
}
)
+ 1
)
coordinator = coordinators[api_key, base] = OpenexchangeratesCoordinator(
hass,
async_get_clientsession(hass),
api_key,
base,
update_interval,
)
if response.status_code != HTTPStatus.OK:
_LOGGER.error("Check your OpenExchangeRates API key")
return
LOGGER.debug(
"Coordinator update interval set to: %s", coordinator.update_interval
)
rest.update()
add_entities([OpenexchangeratesSensor(rest, name, quote)], True)
# Set new interval on all coordinators for this API key.
for (
coordinator_api_key,
_,
), coordinator in coordinators.items():
if coordinator_api_key == api_key:
coordinator.update_interval = update_interval
coordinator = coordinators[api_key, base]
async with coordinator.setup_lock:
# We need to make sure that the coordinator data is ready.
if not coordinator.data:
await coordinator.async_refresh()
if not coordinator.last_update_success:
raise PlatformNotReady
async_add_entities([OpenexchangeratesSensor(coordinator, name, quote)])
class OpenexchangeratesSensor(SensorEntity):
class OpenexchangeratesSensor(
CoordinatorEntity[OpenexchangeratesCoordinator], SensorEntity
):
"""Representation of an Open Exchange Rates sensor."""
_attr_attribution = ATTRIBUTION
def __init__(self, rest: OpenexchangeratesData, name: str, quote: str) -> None:
def __init__(
self, coordinator: OpenexchangeratesCoordinator, name: str, quote: str
) -> None:
"""Initialize the sensor."""
self.rest = rest
self._name = name
super().__init__(coordinator)
self._attr_name = name
self._quote = quote
self._state: float | None = None
self._attr_native_unit_of_measurement = quote
@property
def name(self) -> str:
"""Return the name of the sensor."""
return self._name
@property
def native_value(self) -> float | None:
def native_value(self) -> float:
"""Return the state of the sensor."""
return self._state
return round(self.coordinator.data.rates[self._quote], 4)
@property
def extra_state_attributes(self) -> dict[str, Any] | None:
def extra_state_attributes(self) -> dict[str, float]:
"""Return other attributes of the sensor."""
attr = self.rest.data
return attr
def update(self) -> None:
"""Update current conditions."""
self.rest.update()
if (value := self.rest.data) is None:
self._attr_available = False
return
self._attr_available = True
self._state = round(value[self._quote], 4)
class OpenexchangeratesData:
"""Get data from Openexchangerates.org."""
def __init__(self, resource: str, parameters: dict[str, str], quote: str) -> None:
"""Initialize the data object."""
self._resource = resource
self._parameters = parameters
self._quote = quote
self.data: dict[str, Any] | None = None
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self) -> None:
"""Get the latest data from openexchangerates.org."""
try:
result = requests.get(self._resource, params=self._parameters, timeout=10)
self.data = result.json()["rates"]
except requests.exceptions.HTTPError:
_LOGGER.error("Check the Openexchangerates API key")
self.data = None
return self.coordinator.data.rates

View File

@ -219,6 +219,9 @@ aionotion==3.0.2
# homeassistant.components.oncue
aiooncue==0.3.4
# homeassistant.components.openexchangerates
aioopenexchangerates==0.3.0
# homeassistant.components.acmeda
aiopulse==0.4.3