Add full typing to kraken (#50718)

* Add full typing to kraken

* Let device_info return DeviceInfo

* Replace unsub_listeners with entry.async_on_unload

* Raise TypeError on end of _try_get_state

* Assert Coordinator is not none

* Add class SensorType

* Add strict typing to kraken

* Add changes from code review

* Revert typed dict creation
This commit is contained in:
Kevin Eifinger 2021-05-17 09:12:04 +02:00 committed by GitHub
parent 120bf8aed7
commit 663c0374ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 137 additions and 80 deletions

View file

@ -29,6 +29,7 @@ homeassistant.components.hyperion.*
homeassistant.components.image_processing.* homeassistant.components.image_processing.*
homeassistant.components.integration.* homeassistant.components.integration.*
homeassistant.components.knx.* homeassistant.components.knx.*
homeassistant.components.kraken.*
homeassistant.components.light.* homeassistant.components.light.*
homeassistant.components.lock.* homeassistant.components.lock.*
homeassistant.components.mailbox.* homeassistant.components.mailbox.*

View file

@ -21,6 +21,7 @@ from .const import (
DEFAULT_TRACKED_ASSET_PAIR, DEFAULT_TRACKED_ASSET_PAIR,
DISPATCH_CONFIG_UPDATED, DISPATCH_CONFIG_UPDATED,
DOMAIN, DOMAIN,
KrakenResponse,
) )
from .utils import get_tradable_asset_pairs from .utils import get_tradable_asset_pairs
@ -47,8 +48,6 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
config_entry, PLATFORMS config_entry, PLATFORMS
) )
if unload_ok: if unload_ok:
for unsub_listener in hass.data[DOMAIN].unsub_listeners:
unsub_listener()
hass.data.pop(DOMAIN) hass.data.pop(DOMAIN)
return unload_ok return unload_ok
@ -62,11 +61,10 @@ class KrakenData:
self._hass = hass self._hass = hass
self._config_entry = config_entry self._config_entry = config_entry
self._api = pykrakenapi.KrakenAPI(krakenex.API(), retry=0, crl_sleep=0) self._api = pykrakenapi.KrakenAPI(krakenex.API(), retry=0, crl_sleep=0)
self.tradable_asset_pairs = None self.tradable_asset_pairs: dict[str, str] = {}
self.coordinator = None self.coordinator: DataUpdateCoordinator[KrakenResponse | None] | None = None
self.unsub_listeners = []
async def async_update(self) -> None: async def async_update(self) -> KrakenResponse | None:
"""Get the latest data from the Kraken.com REST API. """Get the latest data from the Kraken.com REST API.
All tradeable asset pairs are retrieved, not the tracked asset pairs All tradeable asset pairs are retrieved, not the tracked asset pairs
@ -91,8 +89,9 @@ class KrakenData:
_LOGGER.warning( _LOGGER.warning(
"Exceeded the Kraken.com call rate limit. Increase the update interval to prevent this error" "Exceeded the Kraken.com call rate limit. Increase the update interval to prevent this error"
) )
return None
def _get_kraken_data(self) -> dict: def _get_kraken_data(self) -> KrakenResponse:
websocket_name_pairs = self._get_websocket_name_asset_pairs() websocket_name_pairs = self._get_websocket_name_asset_pairs()
ticker_df = self._api.get_ticker_information(websocket_name_pairs) ticker_df = self._api.get_ticker_information(websocket_name_pairs)
# Rename columns to their full name # Rename columns to their full name
@ -109,7 +108,7 @@ class KrakenData:
"o": "opening_price", "o": "opening_price",
} }
) )
response_dict = ticker_df.transpose().to_dict() response_dict: KrakenResponse = ticker_df.transpose().to_dict()
return response_dict return response_dict
async def _async_refresh_tradable_asset_pairs(self) -> None: async def _async_refresh_tradable_asset_pairs(self) -> None:
@ -140,12 +139,13 @@ class KrakenData:
) )
await self.coordinator.async_config_entry_first_refresh() await self.coordinator.async_config_entry_first_refresh()
def _get_websocket_name_asset_pairs(self) -> list: def _get_websocket_name_asset_pairs(self) -> str:
return ",".join(wsname for wsname in self.tradable_asset_pairs.values()) return ",".join(wsname for wsname in self.tradable_asset_pairs.values())
def set_update_interval(self, update_interval: int) -> None: def set_update_interval(self, update_interval: int) -> None:
"""Set the coordinator update_interval to the supplied update_interval.""" """Set the coordinator update_interval to the supplied update_interval."""
self.coordinator.update_interval = timedelta(seconds=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: async def async_options_updated(hass: HomeAssistant, config_entry: ConfigEntry) -> None:

View file

@ -1,5 +1,8 @@
"""Config flow for kraken integration.""" """Config flow for kraken integration."""
from __future__ import annotations
import logging import logging
from typing import Any
import krakenex import krakenex
from pykrakenapi.pykrakenapi import KrakenAPI from pykrakenapi.pykrakenapi import KrakenAPI
@ -8,6 +11,7 @@ import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.const import CONF_SCAN_INTERVAL from homeassistant.const import CONF_SCAN_INTERVAL
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from .const import CONF_TRACKED_ASSET_PAIRS, DEFAULT_SCAN_INTERVAL, DOMAIN from .const import CONF_TRACKED_ASSET_PAIRS, DEFAULT_SCAN_INTERVAL, DOMAIN
@ -24,11 +28,15 @@ class KrakenConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
@staticmethod @staticmethod
@callback @callback
def async_get_options_flow(config_entry): def async_get_options_flow(
config_entry: config_entries.ConfigEntry,
) -> KrakenOptionsFlowHandler:
"""Get the options flow for this handler.""" """Get the options flow for this handler."""
return KrakenOptionsFlowHandler(config_entry) return KrakenOptionsFlowHandler(config_entry)
async def async_step_user(self, user_input=None): async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step.""" """Handle the initial step."""
if DOMAIN in self.hass.data: if DOMAIN in self.hass.data:
return self.async_abort(reason="already_configured") return self.async_abort(reason="already_configured")
@ -44,11 +52,13 @@ class KrakenConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
class KrakenOptionsFlowHandler(config_entries.OptionsFlow): class KrakenOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle Kraken client options.""" """Handle Kraken client options."""
def __init__(self, config_entry): def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize Kraken options flow.""" """Initialize Kraken options flow."""
self.config_entry = config_entry self.config_entry = config_entry
async def async_step_init(self, user_input=None): async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage the Kraken options.""" """Manage the Kraken options."""
if user_input is not None: if user_input is not None:
return self.async_create_entry(title="", data=user_input) return self.async_create_entry(title="", data=user_input)

View file

@ -1,5 +1,19 @@
"""Constants for the kraken integration.""" """Constants for the kraken integration."""
from __future__ import annotations
from typing import Dict, TypedDict
KrakenResponse = Dict[str, Dict[str, float]]
class SensorType(TypedDict):
"""SensorType class."""
name: str
enabled_by_default: bool
DEFAULT_SCAN_INTERVAL = 60 DEFAULT_SCAN_INTERVAL = 60
DEFAULT_TRACKED_ASSET_PAIR = "XBT/USD" DEFAULT_TRACKED_ASSET_PAIR = "XBT/USD"
DISPATCH_CONFIG_UPDATED = "kraken_config_updated" DISPATCH_CONFIG_UPDATED = "kraken_config_updated"
@ -8,7 +22,7 @@ CONF_TRACKED_ASSET_PAIRS = "tracked_asset_pairs"
DOMAIN = "kraken" DOMAIN = "kraken"
SENSOR_TYPES = [ SENSOR_TYPES: list[SensorType] = [
{"name": "ask", "enabled_by_default": True}, {"name": "ask", "enabled_by_default": True},
{"name": "ask_volume", "enabled_by_default": False}, {"name": "ask_volume", "enabled_by_default": False},
{"name": "bid", "enabled_by_default": True}, {"name": "bid", "enabled_by_default": True},

View file

@ -8,6 +8,9 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry from homeassistant.helpers import device_registry
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import KrakenData from . import KrakenData
@ -16,12 +19,17 @@ from .const import (
DISPATCH_CONFIG_UPDATED, DISPATCH_CONFIG_UPDATED,
DOMAIN, DOMAIN,
SENSOR_TYPES, SENSOR_TYPES,
SensorType,
) )
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Add kraken entities from a config_entry.""" """Add kraken entities from a config_entry."""
@callback @callback
@ -59,7 +67,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
async_update_sensors(hass, config_entry) async_update_sensors(hass, config_entry)
hass.data[DOMAIN].unsub_listeners.append( config_entry.async_on_unload(
async_dispatcher_connect( async_dispatcher_connect(
hass, hass,
DISPATCH_CONFIG_UPDATED, DISPATCH_CONFIG_UPDATED,
@ -75,9 +83,10 @@ class KrakenSensor(CoordinatorEntity, SensorEntity):
self, self,
kraken_data: KrakenData, kraken_data: KrakenData,
tracked_asset_pair: str, tracked_asset_pair: str,
sensor_type: dict[str, bool], sensor_type: SensorType,
) -> None: ) -> None:
"""Initialize.""" """Initialize."""
assert kraken_data.coordinator is not None
super().__init__(kraken_data.coordinator) super().__init__(kraken_data.coordinator)
self.tracked_asset_pair_wsname = kraken_data.tradable_asset_pairs[ self.tracked_asset_pair_wsname = kraken_data.tradable_asset_pairs[
tracked_asset_pair tracked_asset_pair
@ -100,22 +109,22 @@ class KrakenSensor(CoordinatorEntity, SensorEntity):
self._state = None self._state = None
@property @property
def entity_registry_enabled_default(self): def entity_registry_enabled_default(self) -> bool:
"""Return if the entity should be enabled when first added to the entity registry.""" """Return if the entity should be enabled when first added to the entity registry."""
return self._enabled_by_default return self._enabled_by_default
@property @property
def name(self): def name(self) -> str:
"""Return the name.""" """Return the name."""
return self._name return self._name
@property @property
def unique_id(self): def unique_id(self) -> str:
"""Set unique_id for sensor.""" """Set unique_id for sensor."""
return self._name.lower() return self._name.lower()
@property @property
def state(self): def state(self) -> StateType:
"""Return the state.""" """Return the state."""
return self._state return self._state
@ -124,13 +133,76 @@ class KrakenSensor(CoordinatorEntity, SensorEntity):
await super().async_added_to_hass() await super().async_added_to_hass()
self._update_internal_state() self._update_internal_state()
def _handle_coordinator_update(self): def _handle_coordinator_update(self) -> None:
self._update_internal_state() self._update_internal_state()
super()._handle_coordinator_update() super()._handle_coordinator_update()
def _update_internal_state(self): def _update_internal_state(self) -> None:
try: try:
self._state = self._try_get_state() if self._sensor_type == "last_trade_closed":
self._state = self.coordinator.data[self.tracked_asset_pair_wsname][
"last_trade_closed"
][0]
if self._sensor_type == "ask":
self._state = self.coordinator.data[self.tracked_asset_pair_wsname][
"ask"
][0]
if self._sensor_type == "ask_volume":
self._state = self.coordinator.data[self.tracked_asset_pair_wsname][
"ask"
][1]
if self._sensor_type == "bid":
self._state = self.coordinator.data[self.tracked_asset_pair_wsname][
"bid"
][0]
if self._sensor_type == "bid_volume":
self._state = self.coordinator.data[self.tracked_asset_pair_wsname][
"bid"
][1]
if self._sensor_type == "volume_today":
self._state = self.coordinator.data[self.tracked_asset_pair_wsname][
"volume"
][0]
if self._sensor_type == "volume_last_24h":
self._state = self.coordinator.data[self.tracked_asset_pair_wsname][
"volume"
][1]
if self._sensor_type == "volume_weighted_average_today":
self._state = self.coordinator.data[self.tracked_asset_pair_wsname][
"volume_weighted_average"
][0]
if self._sensor_type == "volume_weighted_average_last_24h":
self._state = self.coordinator.data[self.tracked_asset_pair_wsname][
"volume_weighted_average"
][1]
if self._sensor_type == "number_of_trades_today":
self._state = self.coordinator.data[self.tracked_asset_pair_wsname][
"number_of_trades"
][0]
if self._sensor_type == "number_of_trades_last_24h":
self._state = self.coordinator.data[self.tracked_asset_pair_wsname][
"number_of_trades"
][1]
if self._sensor_type == "low_today":
self._state = self.coordinator.data[self.tracked_asset_pair_wsname][
"low"
][0]
if self._sensor_type == "low_last_24h":
self._state = self.coordinator.data[self.tracked_asset_pair_wsname][
"low"
][1]
if self._sensor_type == "high_today":
self._state = self.coordinator.data[self.tracked_asset_pair_wsname][
"high"
][0]
if self._sensor_type == "high_last_24h":
self._state = self.coordinator.data[self.tracked_asset_pair_wsname][
"high"
][1]
if self._sensor_type == "opening_price_today":
self._state = self.coordinator.data[self.tracked_asset_pair_wsname][
"opening_price"
]
self._received_data_at_least_once = True # Received data at least one time. self._received_data_at_least_once = True # Received data at least one time.
except TypeError: except TypeError:
if self._received_data_at_least_once: if self._received_data_at_least_once:
@ -141,55 +213,8 @@ class KrakenSensor(CoordinatorEntity, SensorEntity):
) )
self._available = False self._available = False
def _try_get_state(self) -> str:
"""Try to get the state or return a TypeError."""
if self._sensor_type == "last_trade_closed":
return self.coordinator.data[self.tracked_asset_pair_wsname][
"last_trade_closed"
][0]
if self._sensor_type == "ask":
return self.coordinator.data[self.tracked_asset_pair_wsname]["ask"][0]
if self._sensor_type == "ask_volume":
return self.coordinator.data[self.tracked_asset_pair_wsname]["ask"][1]
if self._sensor_type == "bid":
return self.coordinator.data[self.tracked_asset_pair_wsname]["bid"][0]
if self._sensor_type == "bid_volume":
return self.coordinator.data[self.tracked_asset_pair_wsname]["bid"][1]
if self._sensor_type == "volume_today":
return self.coordinator.data[self.tracked_asset_pair_wsname]["volume"][0]
if self._sensor_type == "volume_last_24h":
return self.coordinator.data[self.tracked_asset_pair_wsname]["volume"][1]
if self._sensor_type == "volume_weighted_average_today":
return self.coordinator.data[self.tracked_asset_pair_wsname][
"volume_weighted_average"
][0]
if self._sensor_type == "volume_weighted_average_last_24h":
return self.coordinator.data[self.tracked_asset_pair_wsname][
"volume_weighted_average"
][1]
if self._sensor_type == "number_of_trades_today":
return self.coordinator.data[self.tracked_asset_pair_wsname][
"number_of_trades"
][0]
if self._sensor_type == "number_of_trades_last_24h":
return self.coordinator.data[self.tracked_asset_pair_wsname][
"number_of_trades"
][1]
if self._sensor_type == "low_today":
return self.coordinator.data[self.tracked_asset_pair_wsname]["low"][0]
if self._sensor_type == "low_last_24h":
return self.coordinator.data[self.tracked_asset_pair_wsname]["low"][1]
if self._sensor_type == "high_today":
return self.coordinator.data[self.tracked_asset_pair_wsname]["high"][0]
if self._sensor_type == "high_last_24h":
return self.coordinator.data[self.tracked_asset_pair_wsname]["high"][1]
if self._sensor_type == "opening_price_today":
return self.coordinator.data[self.tracked_asset_pair_wsname][
"opening_price"
]
@property @property
def icon(self): def icon(self) -> str:
"""Return the icon.""" """Return the icon."""
if self._target_asset == "EUR": if self._target_asset == "EUR":
return "mdi:currency-eur" return "mdi:currency-eur"
@ -204,19 +229,19 @@ class KrakenSensor(CoordinatorEntity, SensorEntity):
return "mdi:cash" return "mdi:cash"
@property @property
def unit_of_measurement(self): def unit_of_measurement(self) -> str | None:
"""Return the unit the value is expressed in.""" """Return the unit the value is expressed in."""
if "number_of" not in self._sensor_type: if "number_of" not in self._sensor_type:
return self._unit_of_measurement return self._unit_of_measurement
return None return None
@property @property
def available(self): def available(self) -> bool:
"""Could the api be accessed during the last update call.""" """Could the api be accessed during the last update call."""
return self._available and self.coordinator.last_update_success return self._available and self.coordinator.last_update_success
@property @property
def device_info(self) -> dict: def device_info(self) -> DeviceInfo:
"""Return a device description for device registry.""" """Return a device description for device registry."""
return { return {

View file

@ -330,6 +330,17 @@ no_implicit_optional = true
warn_return_any = true warn_return_any = true
warn_unreachable = true warn_unreachable = true
[mypy-homeassistant.components.kraken.*]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
no_implicit_optional = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.light.*] [mypy-homeassistant.components.light.*]
check_untyped_defs = true check_untyped_defs = true
disallow_incomplete_defs = true disallow_incomplete_defs = true
@ -982,9 +993,6 @@ ignore_errors = true
[mypy-homeassistant.components.kostal_plenticore.*] [mypy-homeassistant.components.kostal_plenticore.*]
ignore_errors = true ignore_errors = true
[mypy-homeassistant.components.kraken.*]
ignore_errors = true
[mypy-homeassistant.components.kulersky.*] [mypy-homeassistant.components.kulersky.*]
ignore_errors = true ignore_errors = true

View file

@ -114,7 +114,6 @@ IGNORED_MODULES: Final[list[str]] = [
"homeassistant.components.kodi.*", "homeassistant.components.kodi.*",
"homeassistant.components.konnected.*", "homeassistant.components.konnected.*",
"homeassistant.components.kostal_plenticore.*", "homeassistant.components.kostal_plenticore.*",
"homeassistant.components.kraken.*",
"homeassistant.components.kulersky.*", "homeassistant.components.kulersky.*",
"homeassistant.components.lifx.*", "homeassistant.components.lifx.*",
"homeassistant.components.litejet.*", "homeassistant.components.litejet.*",