159 lines
5.8 KiB
Python
159 lines
5.8 KiB
Python
"""The kraken integration."""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from datetime import timedelta
|
|
import logging
|
|
|
|
import krakenex
|
|
import pykrakenapi
|
|
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.const import CONF_SCAN_INTERVAL, Platform
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
|
|
|
from .const import (
|
|
CONF_TRACKED_ASSET_PAIRS,
|
|
DEFAULT_SCAN_INTERVAL,
|
|
DEFAULT_TRACKED_ASSET_PAIR,
|
|
DISPATCH_CONFIG_UPDATED,
|
|
DOMAIN,
|
|
KrakenResponse,
|
|
)
|
|
from .utils import get_tradable_asset_pairs
|
|
|
|
CALL_RATE_LIMIT_SLEEP = 1
|
|
|
|
PLATFORMS = [Platform.SENSOR]
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
"""Set up kraken from a config entry."""
|
|
kraken_data = KrakenData(hass, entry)
|
|
await kraken_data.async_setup()
|
|
hass.data[DOMAIN] = kraken_data
|
|
entry.async_on_unload(entry.add_update_listener(async_options_updated))
|
|
await hass.config_entries.async_forward_entry_setups(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
|
|
)
|
|
if unload_ok:
|
|
hass.data.pop(DOMAIN)
|
|
|
|
return unload_ok
|
|
|
|
|
|
class KrakenData:
|
|
"""Define an object to hold kraken data."""
|
|
|
|
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
|
"""Initialize."""
|
|
self._hass = hass
|
|
self._config_entry = config_entry
|
|
self._api = pykrakenapi.KrakenAPI(krakenex.API(), retry=0, crl_sleep=0)
|
|
self.tradable_asset_pairs: dict[str, str] = {}
|
|
self.coordinator: DataUpdateCoordinator[KrakenResponse | None] | None = None
|
|
|
|
async def async_update(self) -> KrakenResponse | None:
|
|
"""Get the latest data from the Kraken.com REST API.
|
|
|
|
All tradeable asset pairs are retrieved, not the tracked asset pairs
|
|
selected by the user. This enables us to check for an unknown and
|
|
thus likely removed asset pair in sensor.py and only log a warning
|
|
once.
|
|
"""
|
|
try:
|
|
async with asyncio.timeout(10):
|
|
return await self._hass.async_add_executor_job(self._get_kraken_data)
|
|
except pykrakenapi.pykrakenapi.KrakenAPIError as error:
|
|
if "Unknown asset pair" in str(error):
|
|
_LOGGER.info(
|
|
"Kraken.com reported an unknown asset pair. Refreshing list of"
|
|
" tradable asset pairs"
|
|
)
|
|
await self._async_refresh_tradable_asset_pairs()
|
|
else:
|
|
raise UpdateFailed(
|
|
f"Unable to fetch data from Kraken.com: {error}"
|
|
) from error
|
|
except pykrakenapi.pykrakenapi.CallRateLimitError:
|
|
_LOGGER.warning(
|
|
"Exceeded the Kraken.com call rate limit. Increase the update interval"
|
|
" to prevent this error"
|
|
)
|
|
return None
|
|
|
|
def _get_kraken_data(self) -> KrakenResponse:
|
|
websocket_name_pairs = self._get_websocket_name_asset_pairs()
|
|
ticker_df = self._api.get_ticker_information(websocket_name_pairs)
|
|
# Rename columns to their full name
|
|
ticker_df = ticker_df.rename(
|
|
columns={
|
|
"a": "ask",
|
|
"b": "bid",
|
|
"c": "last_trade_closed",
|
|
"v": "volume",
|
|
"p": "volume_weighted_average",
|
|
"t": "number_of_trades",
|
|
"l": "low",
|
|
"h": "high",
|
|
"o": "opening_price",
|
|
}
|
|
)
|
|
response_dict: KrakenResponse = ticker_df.transpose().to_dict()
|
|
return response_dict
|
|
|
|
async def _async_refresh_tradable_asset_pairs(self) -> None:
|
|
self.tradable_asset_pairs = await self._hass.async_add_executor_job(
|
|
get_tradable_asset_pairs, self._api
|
|
)
|
|
|
|
async def async_setup(self) -> None:
|
|
"""Set up the Kraken integration."""
|
|
if not self._config_entry.options:
|
|
options = {
|
|
CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL,
|
|
CONF_TRACKED_ASSET_PAIRS: [DEFAULT_TRACKED_ASSET_PAIR],
|
|
}
|
|
self._hass.config_entries.async_update_entry(
|
|
self._config_entry, options=options
|
|
)
|
|
await self._async_refresh_tradable_asset_pairs()
|
|
# Wait 1 second to avoid triggering the KrakenAPI CallRateLimiter
|
|
await asyncio.sleep(CALL_RATE_LIMIT_SLEEP)
|
|
self.coordinator = DataUpdateCoordinator(
|
|
self._hass,
|
|
_LOGGER,
|
|
name=DOMAIN,
|
|
update_method=self.async_update,
|
|
update_interval=timedelta(
|
|
seconds=self._config_entry.options[CONF_SCAN_INTERVAL]
|
|
),
|
|
)
|
|
await self.coordinator.async_config_entry_first_refresh()
|
|
# Wait 1 second to avoid triggering the KrakenAPI CallRateLimiter
|
|
await asyncio.sleep(CALL_RATE_LIMIT_SLEEP)
|
|
|
|
def _get_websocket_name_asset_pairs(self) -> str:
|
|
return ",".join(wsname for wsname in self.tradable_asset_pairs.values())
|
|
|
|
def set_update_interval(self, update_interval: int) -> None:
|
|
"""Set the coordinator update_interval to the supplied update_interval."""
|
|
if self.coordinator is not None:
|
|
self.coordinator.update_interval = timedelta(seconds=update_interval)
|
|
|
|
|
|
async def async_options_updated(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
|
"""Triggered by config entry options updates."""
|
|
hass.data[DOMAIN].set_update_interval(config_entry.options[CONF_SCAN_INTERVAL])
|
|
async_dispatcher_send(hass, DISPATCH_CONFIG_UPDATED, hass, config_entry)
|