Add config flow for Coinbase (#45354)
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
This commit is contained in:
parent
efee36a176
commit
fd1d110b80
16 changed files with 1299 additions and 96 deletions
|
@ -154,7 +154,7 @@ omit =
|
|||
homeassistant/components/clicksend_tts/notify.py
|
||||
homeassistant/components/cmus/media_player.py
|
||||
homeassistant/components/co2signal/*
|
||||
homeassistant/components/coinbase/*
|
||||
homeassistant/components/coinbase/sensor.py
|
||||
homeassistant/components/comed_hourly_pricing/sensor.py
|
||||
homeassistant/components/comfoconnect/fan.py
|
||||
homeassistant/components/concord232/alarm_control_panel.py
|
||||
|
|
|
@ -88,6 +88,7 @@ homeassistant/components/cisco_webex_teams/* @fbradyirl
|
|||
homeassistant/components/climacell/* @raman325
|
||||
homeassistant/components/cloud/* @home-assistant/cloud
|
||||
homeassistant/components/cloudflare/* @ludeeus @ctalkington
|
||||
homeassistant/components/coinbase/* @tombrien
|
||||
homeassistant/components/color_extractor/* @GenericStudent
|
||||
homeassistant/components/comfoconnect/* @michaelarnauts
|
||||
homeassistant/components/compensation/* @Petro31
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
"""Support for Coinbase."""
|
||||
"""The Coinbase integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
|
@ -6,105 +8,121 @@ from coinbase.wallet.client import Client
|
|||
from coinbase.wallet.error import AuthenticationError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.discovery import load_platform
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
from .const import (
|
||||
API_ACCOUNT_ID,
|
||||
API_ACCOUNTS_DATA,
|
||||
CONF_CURRENCIES,
|
||||
CONF_EXCHANGE_RATES,
|
||||
CONF_YAML_API_TOKEN,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "coinbase"
|
||||
|
||||
CONF_API_SECRET = "api_secret"
|
||||
CONF_ACCOUNT_CURRENCIES = "account_balance_currencies"
|
||||
CONF_EXCHANGE_CURRENCIES = "exchange_rate_currencies"
|
||||
|
||||
PLATFORMS = ["sensor"]
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1)
|
||||
|
||||
DATA_COINBASE = "coinbase_cache"
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
cv.deprecated(DOMAIN),
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_API_KEY): cv.string,
|
||||
vol.Required(CONF_API_SECRET): cv.string,
|
||||
vol.Optional(CONF_ACCOUNT_CURRENCIES): vol.All(
|
||||
vol.Required(CONF_YAML_API_TOKEN): cv.string,
|
||||
vol.Optional(CONF_CURRENCIES): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_EXCHANGE_RATES, default=[]): vol.All(
|
||||
cv.ensure_list, [cv.string]
|
||||
),
|
||||
vol.Optional(CONF_EXCHANGE_CURRENCIES, default=[]): vol.All(
|
||||
cv.ensure_list, [cv.string]
|
||||
),
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Set up the Coinbase component.
|
||||
|
||||
Will automatically setup sensors to support
|
||||
wallets discovered on the network.
|
||||
"""
|
||||
api_key = config[DOMAIN][CONF_API_KEY]
|
||||
api_secret = config[DOMAIN][CONF_API_SECRET]
|
||||
account_currencies = config[DOMAIN].get(CONF_ACCOUNT_CURRENCIES)
|
||||
exchange_currencies = config[DOMAIN][CONF_EXCHANGE_CURRENCIES]
|
||||
|
||||
hass.data[DATA_COINBASE] = coinbase_data = CoinbaseData(api_key, api_secret)
|
||||
|
||||
if not hasattr(coinbase_data, "accounts"):
|
||||
return False
|
||||
for account in coinbase_data.accounts:
|
||||
if account_currencies is None or account.currency in account_currencies:
|
||||
load_platform(hass, "sensor", DOMAIN, {"account": account}, config)
|
||||
for currency in exchange_currencies:
|
||||
if currency not in coinbase_data.exchange_rates.rates:
|
||||
_LOGGER.warning("Currency %s not found", currency)
|
||||
continue
|
||||
native = coinbase_data.exchange_rates.currency
|
||||
load_platform(
|
||||
hass,
|
||||
"sensor",
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Coinbase component."""
|
||||
if DOMAIN not in config:
|
||||
return True
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
{"native_currency": native, "exchange_currency": currency},
|
||||
config,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=config[DOMAIN],
|
||||
)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Coinbase from a config entry."""
|
||||
|
||||
client = await hass.async_add_executor_job(
|
||||
Client,
|
||||
entry.data[CONF_API_KEY],
|
||||
entry.data[CONF_API_TOKEN],
|
||||
)
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = await hass.async_add_executor_job(
|
||||
CoinbaseData, client
|
||||
)
|
||||
|
||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
def get_accounts(client):
|
||||
"""Handle paginated accounts."""
|
||||
response = client.get_accounts()
|
||||
accounts = response[API_ACCOUNTS_DATA]
|
||||
next_starting_after = response.pagination.next_starting_after
|
||||
|
||||
while next_starting_after:
|
||||
response = client.get_accounts(starting_after=next_starting_after)
|
||||
accounts += response[API_ACCOUNTS_DATA]
|
||||
next_starting_after = response.pagination.next_starting_after
|
||||
|
||||
return accounts
|
||||
|
||||
|
||||
class CoinbaseData:
|
||||
"""Get the latest data and update the states."""
|
||||
|
||||
def __init__(self, api_key, api_secret):
|
||||
def __init__(self, client):
|
||||
"""Init the coinbase data object."""
|
||||
|
||||
self.client = Client(api_key, api_secret)
|
||||
self.update()
|
||||
self.client = client
|
||||
self.accounts = get_accounts(self.client)
|
||||
self.exchange_rates = self.client.get_exchange_rates()
|
||||
self.user_id = self.client.get_current_user()[API_ACCOUNT_ID]
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data from coinbase."""
|
||||
|
||||
try:
|
||||
response = self.client.get_accounts()
|
||||
accounts = response["data"]
|
||||
|
||||
# Most of Coinbase's API seems paginated now (25 items per page, but first page has 24).
|
||||
# This API gives a 'next_starting_after' property to send back as a 'starting_after' param.
|
||||
# Their API documentation is not up to date when writing these lines (2021-05-20)
|
||||
next_starting_after = response.pagination.next_starting_after
|
||||
|
||||
while next_starting_after:
|
||||
response = self.client.get_accounts(starting_after=next_starting_after)
|
||||
accounts = accounts + response["data"]
|
||||
next_starting_after = response.pagination.next_starting_after
|
||||
|
||||
self.accounts = accounts
|
||||
|
||||
self.accounts = get_accounts(self.client)
|
||||
self.exchange_rates = self.client.get_exchange_rates()
|
||||
except AuthenticationError as coinbase_error:
|
||||
_LOGGER.error(
|
||||
|
|
211
homeassistant/components/coinbase/config_flow.py
Normal file
211
homeassistant/components/coinbase/config_flow.py
Normal file
|
@ -0,0 +1,211 @@
|
|||
"""Config flow for Coinbase integration."""
|
||||
import logging
|
||||
|
||||
from coinbase.wallet.client import Client
|
||||
from coinbase.wallet.error import AuthenticationError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries, core, exceptions
|
||||
from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from . import get_accounts
|
||||
from .const import (
|
||||
API_ACCOUNT_CURRENCY,
|
||||
API_RATES,
|
||||
CONF_CURRENCIES,
|
||||
CONF_EXCHANGE_RATES,
|
||||
CONF_OPTIONS,
|
||||
CONF_YAML_API_TOKEN,
|
||||
DOMAIN,
|
||||
RATES,
|
||||
WALLETS,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
vol.Required(CONF_API_TOKEN): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def validate_api(hass: core.HomeAssistant, data):
|
||||
"""Validate the credentials."""
|
||||
|
||||
for entry in hass.config_entries.async_entries(DOMAIN):
|
||||
if entry.data[CONF_API_KEY] == data[CONF_API_KEY]:
|
||||
raise AlreadyConfigured
|
||||
try:
|
||||
client = await hass.async_add_executor_job(
|
||||
Client, data[CONF_API_KEY], data[CONF_API_TOKEN]
|
||||
)
|
||||
user = await hass.async_add_executor_job(client.get_current_user)
|
||||
except AuthenticationError as error:
|
||||
raise InvalidAuth from error
|
||||
except ConnectionError as error:
|
||||
raise CannotConnect from error
|
||||
|
||||
return {"title": user["name"]}
|
||||
|
||||
|
||||
async def validate_options(
|
||||
hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry, options
|
||||
):
|
||||
"""Validate the requested resources are provided by API."""
|
||||
|
||||
client = hass.data[DOMAIN][config_entry.entry_id].client
|
||||
|
||||
accounts = await hass.async_add_executor_job(get_accounts, client)
|
||||
|
||||
accounts_currencies = [account[API_ACCOUNT_CURRENCY] for account in accounts]
|
||||
available_rates = await hass.async_add_executor_job(client.get_exchange_rates)
|
||||
if CONF_CURRENCIES in options:
|
||||
for currency in options[CONF_CURRENCIES]:
|
||||
if currency not in accounts_currencies:
|
||||
raise CurrencyUnavaliable
|
||||
|
||||
if CONF_EXCHANGE_RATES in options:
|
||||
for rate in options[CONF_EXCHANGE_RATES]:
|
||||
if rate not in available_rates[API_RATES]:
|
||||
raise ExchangeRateUnavaliable
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Coinbase."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle the initial step."""
|
||||
errors = {}
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
options = {}
|
||||
|
||||
if CONF_OPTIONS in user_input:
|
||||
options = user_input.pop(CONF_OPTIONS)
|
||||
|
||||
try:
|
||||
info = await validate_api(self.hass, user_input)
|
||||
except AlreadyConfigured:
|
||||
return self.async_abort(reason="already_configured")
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title=info["title"], data=user_input, options=options
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_import(self, config):
|
||||
"""Handle import of Coinbase config from YAML."""
|
||||
cleaned_data = {
|
||||
CONF_API_KEY: config[CONF_API_KEY],
|
||||
CONF_API_TOKEN: config[CONF_YAML_API_TOKEN],
|
||||
}
|
||||
cleaned_data[CONF_OPTIONS] = {
|
||||
CONF_CURRENCIES: [],
|
||||
CONF_EXCHANGE_RATES: [],
|
||||
}
|
||||
if CONF_CURRENCIES in config:
|
||||
cleaned_data[CONF_OPTIONS][CONF_CURRENCIES] = config[CONF_CURRENCIES]
|
||||
if CONF_EXCHANGE_RATES in config:
|
||||
cleaned_data[CONF_OPTIONS][CONF_EXCHANGE_RATES] = config[
|
||||
CONF_EXCHANGE_RATES
|
||||
]
|
||||
|
||||
return await self.async_step_user(user_input=cleaned_data)
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry):
|
||||
"""Get the options flow for this handler."""
|
||||
return OptionsFlowHandler(config_entry)
|
||||
|
||||
|
||||
class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Handle a option flow for Coinbase."""
|
||||
|
||||
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Manage the options."""
|
||||
|
||||
errors = {}
|
||||
default_currencies = self.config_entry.options.get(CONF_CURRENCIES)
|
||||
default_exchange_rates = self.config_entry.options.get(CONF_EXCHANGE_RATES)
|
||||
|
||||
if user_input is not None:
|
||||
# Pass back user selected options, even if bad
|
||||
if CONF_CURRENCIES in user_input:
|
||||
default_currencies = user_input[CONF_CURRENCIES]
|
||||
|
||||
if CONF_EXCHANGE_RATES in user_input:
|
||||
default_exchange_rates = user_input[CONF_EXCHANGE_RATES]
|
||||
|
||||
try:
|
||||
await validate_options(self.hass, self.config_entry, user_input)
|
||||
except CurrencyUnavaliable:
|
||||
errors["base"] = "currency_unavaliable"
|
||||
except ExchangeRateUnavaliable:
|
||||
errors["base"] = "exchange_rate_unavaliable"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_CURRENCIES,
|
||||
default=default_currencies,
|
||||
): cv.multi_select(WALLETS),
|
||||
vol.Optional(
|
||||
CONF_EXCHANGE_RATES,
|
||||
default=default_exchange_rates,
|
||||
): cv.multi_select(RATES),
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
|
||||
class CannotConnect(exceptions.HomeAssistantError):
|
||||
"""Error to indicate we cannot connect."""
|
||||
|
||||
|
||||
class InvalidAuth(exceptions.HomeAssistantError):
|
||||
"""Error to indicate there is invalid auth."""
|
||||
|
||||
|
||||
class AlreadyConfigured(exceptions.HomeAssistantError):
|
||||
"""Error to indicate Coinbase API Key is already configured."""
|
||||
|
||||
|
||||
class CurrencyUnavaliable(exceptions.HomeAssistantError):
|
||||
"""Error to indicate the requested currency resource is not provided by the API."""
|
||||
|
||||
|
||||
class ExchangeRateUnavaliable(exceptions.HomeAssistantError):
|
||||
"""Error to indicate the requested exchange rate resource is not provided by the API."""
|
290
homeassistant/components/coinbase/const.py
Normal file
290
homeassistant/components/coinbase/const.py
Normal file
|
@ -0,0 +1,290 @@
|
|||
"""Constants used for Coinbase."""
|
||||
|
||||
CONF_CURRENCIES = "account_balance_currencies"
|
||||
CONF_EXCHANGE_RATES = "exchange_rate_currencies"
|
||||
CONF_OPTIONS = "options"
|
||||
DOMAIN = "coinbase"
|
||||
|
||||
# These are constants used by the previous YAML configuration
|
||||
CONF_YAML_API_TOKEN = "api_secret"
|
||||
|
||||
# Constants for data returned by Coinbase API
|
||||
API_ACCOUNT_AMOUNT = "amount"
|
||||
API_ACCOUNT_BALANCE = "balance"
|
||||
API_ACCOUNT_CURRENCY = "currency"
|
||||
API_ACCOUNT_ID = "id"
|
||||
API_ACCOUNT_NATIVE_BALANCE = "native_balance"
|
||||
API_ACCOUNT_NAME = "name"
|
||||
API_ACCOUNTS_DATA = "data"
|
||||
API_RATES = "rates"
|
||||
|
||||
WALLETS = {
|
||||
"AAVE": "AAVE",
|
||||
"ALGO": "ALGO",
|
||||
"ATOM": "ATOM",
|
||||
"BAL": "BAL",
|
||||
"BAND": "BAND",
|
||||
"BAT": "BAT",
|
||||
"BCH": "BCH",
|
||||
"BNT": "BNT",
|
||||
"BSV": "BSV",
|
||||
"BTC": "BTC",
|
||||
"CGLD": "CLGD",
|
||||
"CVC": "CVC",
|
||||
"COMP": "COMP",
|
||||
"DAI": "DAI",
|
||||
"DNT": "DNT",
|
||||
"EOS": "EOS",
|
||||
"ETC": "ETC",
|
||||
"ETH": "ETH",
|
||||
"EUR": "EUR",
|
||||
"FIL": "FIL",
|
||||
"GBP": "GBP",
|
||||
"GRT": "GRT",
|
||||
"KNC": "KNC",
|
||||
"LINK": "LINK",
|
||||
"LRC": "LRC",
|
||||
"LTC": "LTC",
|
||||
"MANA": "MANA",
|
||||
"MKR": "MKR",
|
||||
"NMR": "NMR",
|
||||
"NU": "NU",
|
||||
"OMG": "OMG",
|
||||
"OXT": "OXT",
|
||||
"REP": "REP",
|
||||
"REPV2": "REPV2",
|
||||
"SNX": "SNX",
|
||||
"UMA": "UMA",
|
||||
"UNI": "UNI",
|
||||
"USDC": "USDC",
|
||||
"WBTC": "WBTC",
|
||||
"XLM": "XLM",
|
||||
"XRP": "XRP",
|
||||
"XTZ": "XTZ",
|
||||
"YFI": "YFI",
|
||||
"ZRX": "ZRX",
|
||||
}
|
||||
|
||||
RATES = {
|
||||
"1INCH": "1INCH",
|
||||
"AAVE": "AAVE",
|
||||
"ADA": "ADA",
|
||||
"AED": "AED",
|
||||
"AFN": "AFN",
|
||||
"ALGO": "ALGO",
|
||||
"ALL": "ALL",
|
||||
"AMD": "AMD",
|
||||
"ANG": "ANG",
|
||||
"ANKR": "ANKR",
|
||||
"AOA": "AOA",
|
||||
"ARS": "ARS",
|
||||
"ATOM": "ATOM",
|
||||
"AUD": "AUD",
|
||||
"AWG": "AWG",
|
||||
"AZN": "AZN",
|
||||
"BAL": "BAL",
|
||||
"BAM": "BAM",
|
||||
"BAND": "BAND",
|
||||
"BAT": "BAT",
|
||||
"BBD": "BBD",
|
||||
"BCH": "BCH",
|
||||
"BDT": "BDT",
|
||||
"BGN": "BGN",
|
||||
"BHD": "BHD",
|
||||
"BIF": "BIF",
|
||||
"BMD": "BMD",
|
||||
"BND": "BND",
|
||||
"BNT": "BNT",
|
||||
"BOB": "BOB",
|
||||
"BRL": "BRL",
|
||||
"BSD": "BSD",
|
||||
"BSV": "BSV",
|
||||
"BTC": "BTC",
|
||||
"BTN": "BTN",
|
||||
"BWP": "BWP",
|
||||
"BYN": "BYN",
|
||||
"BYR": "BYR",
|
||||
"BZD": "BZD",
|
||||
"CAD": "CAD",
|
||||
"CDF": "CDF",
|
||||
"CGLD": "CGLD",
|
||||
"CHF": "CHF",
|
||||
"CLF": "CLF",
|
||||
"CLP": "CLP",
|
||||
"CNH": "CNH",
|
||||
"CNY": "CNY",
|
||||
"COMP": "COMP",
|
||||
"COP": "COP",
|
||||
"CRC": "CRC",
|
||||
"CRV": "CRV",
|
||||
"CUC": "CUC",
|
||||
"CVC": "CVC",
|
||||
"CVE": "CVE",
|
||||
"CZK": "CZK",
|
||||
"DAI": "DAI",
|
||||
"DASH": "DASH",
|
||||
"DJF": "DJF",
|
||||
"DKK": "DKK",
|
||||
"DNT": "DNT",
|
||||
"DOP": "DOP",
|
||||
"DZD": "DZD",
|
||||
"EGP": "EGP",
|
||||
"ENJ": "ENJ",
|
||||
"EOS": "EOS",
|
||||
"ERN": "ERN",
|
||||
"ETB": "ETB",
|
||||
"ETC": "ETC",
|
||||
"ETH": "ETH",
|
||||
"ETH2": "ETH2",
|
||||
"EUR": "EUR",
|
||||
"FIL": "FIL",
|
||||
"FJD": "FJD",
|
||||
"FKP": "FKP",
|
||||
"FORTH": "FORTH",
|
||||
"GBP": "GBP",
|
||||
"GBX": "GBX",
|
||||
"GEL": "GEL",
|
||||
"GGP": "GGP",
|
||||
"GHS": "GHS",
|
||||
"GIP": "GIP",
|
||||
"GMD": "GMD",
|
||||
"GNF": "GNF",
|
||||
"GRT": "GRT",
|
||||
"GTQ": "GTQ",
|
||||
"GYD": "GYD",
|
||||
"HKD": "HKD",
|
||||
"HNL": "HNL",
|
||||
"HRK": "HRK",
|
||||
"HTG": "HTG",
|
||||
"HUF": "HUF",
|
||||
"IDR": "IDR",
|
||||
"ILS": "ILS",
|
||||
"IMP": "IMP",
|
||||
"INR": "INR",
|
||||
"IQD": "IQD",
|
||||
"ISK": "ISK",
|
||||
"JEP": "JEP",
|
||||
"JMD": "JMD",
|
||||
"JOD": "JOD",
|
||||
"JPY": "JPY",
|
||||
"KES": "KES",
|
||||
"KGS": "KGS",
|
||||
"KHR": "KHR",
|
||||
"KMF": "KMF",
|
||||
"KNC": "KNC",
|
||||
"KRW": "KRW",
|
||||
"KWD": "KWD",
|
||||
"KYD": "KYD",
|
||||
"KZT": "KZT",
|
||||
"LAK": "LAK",
|
||||
"LBP": "LBP",
|
||||
"LINK": "LINK",
|
||||
"LKR": "LKR",
|
||||
"LRC": "LRC",
|
||||
"LRD": "LRD",
|
||||
"LSL": "LSL",
|
||||
"LTC": "LTC",
|
||||
"LYD": "LYD",
|
||||
"MAD": "MAD",
|
||||
"MANA": "MANA",
|
||||
"MATIC": "MATIC",
|
||||
"MDL": "MDL",
|
||||
"MGA": "MGA",
|
||||
"MKD": "MKD",
|
||||
"MKR": "MKR",
|
||||
"MMK": "MMK",
|
||||
"MNT": "MNT",
|
||||
"MOP": "MOP",
|
||||
"MRO": "MRO",
|
||||
"MTL": "MTL",
|
||||
"MUR": "MUR",
|
||||
"MVR": "MVR",
|
||||
"MWK": "MWK",
|
||||
"MXN": "MXN",
|
||||
"MYR": "MYR",
|
||||
"MZN": "MZN",
|
||||
"NAD": "NAD",
|
||||
"NGN": "NGN",
|
||||
"NIO": "NIO",
|
||||
"NKN": "NKN",
|
||||
"NMR": "NMR",
|
||||
"NOK": "NOK",
|
||||
"NPR": "NPR",
|
||||
"NU": "NU",
|
||||
"NZD": "NZD",
|
||||
"OGN": "OGN",
|
||||
"OMG": "OMG",
|
||||
"OMR": "OMR",
|
||||
"OXT": "OXT",
|
||||
"PAB": "PAB",
|
||||
"PEN": "PEN",
|
||||
"PGK": "PGK",
|
||||
"PHP": "PHP",
|
||||
"PKR": "PKR",
|
||||
"PLN": "PLN",
|
||||
"PYG": "PYG",
|
||||
"QAR": "QAR",
|
||||
"REN": "REN",
|
||||
"REP": "REP",
|
||||
"RON": "RON",
|
||||
"RSD": "RSD",
|
||||
"RUB": "RUB",
|
||||
"RWF": "RWF",
|
||||
"SAR": "SAR",
|
||||
"SBD": "SBD",
|
||||
"SCR": "SCR",
|
||||
"SEK": "SEK",
|
||||
"SGD": "SGD",
|
||||
"SHP": "SHP",
|
||||
"SKL": "SKL",
|
||||
"SLL": "SLL",
|
||||
"SNX": "SNX",
|
||||
"SOS": "SOS",
|
||||
"SRD": "SRD",
|
||||
"SSP": "SSP",
|
||||
"STD": "STD",
|
||||
"STORJ": "STORJ",
|
||||
"SUSHI": "SUSHI",
|
||||
"SVC": "SVC",
|
||||
"SZL": "SZL",
|
||||
"THB": "THB",
|
||||
"TJS": "TJS",
|
||||
"TMT": "TMT",
|
||||
"TND": "TND",
|
||||
"TOP": "TOP",
|
||||
"TRY": "TRY",
|
||||
"TTD": "TTD",
|
||||
"TWD": "TWD",
|
||||
"TZS": "TZS",
|
||||
"UAH": "UAH",
|
||||
"UGX": "UGX",
|
||||
"UMA": "UMA",
|
||||
"UNI": "UNI",
|
||||
"USD": "USD",
|
||||
"USDC": "USDC",
|
||||
"UYU": "UYU",
|
||||
"UZS": "UZS",
|
||||
"VES": "VES",
|
||||
"VND": "VND",
|
||||
"VUV": "VUV",
|
||||
"WBTC": "WBTC",
|
||||
"WST": "WST",
|
||||
"XAF": "XAF",
|
||||
"XAG": "XAG",
|
||||
"XAU": "XAU",
|
||||
"XCD": "XCD",
|
||||
"XDR": "XDR",
|
||||
"XLM": "XLM",
|
||||
"XOF": "XOF",
|
||||
"XPD": "XPD",
|
||||
"XPF": "XPF",
|
||||
"XPT": "XPT",
|
||||
"XTZ": "XTZ",
|
||||
"YER": "YER",
|
||||
"YFI": "YFI",
|
||||
"ZAR": "ZAR",
|
||||
"ZEC": "ZEC",
|
||||
"ZMW": "ZMW",
|
||||
"ZRX": "ZRX",
|
||||
"ZWL": "ZWL",
|
||||
}
|
|
@ -2,7 +2,12 @@
|
|||
"domain": "coinbase",
|
||||
"name": "Coinbase",
|
||||
"documentation": "https://www.home-assistant.io/integrations/coinbase",
|
||||
"requirements": ["coinbase==2.1.0"],
|
||||
"codeowners": [],
|
||||
"requirements": [
|
||||
"coinbase==2.1.0"
|
||||
],
|
||||
"codeowners": [
|
||||
"@tombrien"
|
||||
],
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
}
|
||||
}
|
|
@ -1,7 +1,23 @@
|
|||
"""Support for Coinbase sensors."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.const import ATTR_ATTRIBUTION
|
||||
|
||||
from .const import (
|
||||
API_ACCOUNT_AMOUNT,
|
||||
API_ACCOUNT_BALANCE,
|
||||
API_ACCOUNT_CURRENCY,
|
||||
API_ACCOUNT_ID,
|
||||
API_ACCOUNT_NAME,
|
||||
API_ACCOUNT_NATIVE_BALANCE,
|
||||
CONF_CURRENCIES,
|
||||
CONF_EXCHANGE_RATES,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_NATIVE_BALANCE = "Balance in native currency"
|
||||
|
||||
CURRENCY_ICONS = {
|
||||
|
@ -16,45 +32,79 @@ DEFAULT_COIN_ICON = "mdi:currency-usd-circle"
|
|||
|
||||
ATTRIBUTION = "Data provided by coinbase.com"
|
||||
|
||||
DATA_COINBASE = "coinbase_cache"
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Set up Coinbase sensor platform."""
|
||||
instance = hass.data[DOMAIN][config_entry.entry_id]
|
||||
hass.async_add_executor_job(instance.update)
|
||||
|
||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
"""Set up the Coinbase sensors."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
if "account" in discovery_info:
|
||||
account = discovery_info["account"]
|
||||
sensor = AccountSensor(
|
||||
hass.data[DATA_COINBASE], account["name"], account["balance"]["currency"]
|
||||
)
|
||||
if "exchange_currency" in discovery_info:
|
||||
sensor = ExchangeRateSensor(
|
||||
hass.data[DATA_COINBASE],
|
||||
discovery_info["exchange_currency"],
|
||||
discovery_info["native_currency"],
|
||||
)
|
||||
entities = []
|
||||
|
||||
add_entities([sensor], True)
|
||||
provided_currencies = [
|
||||
account[API_ACCOUNT_CURRENCY] for account in instance.accounts
|
||||
]
|
||||
|
||||
if CONF_CURRENCIES in config_entry.options:
|
||||
desired_currencies = config_entry.options[CONF_CURRENCIES]
|
||||
else:
|
||||
desired_currencies = provided_currencies
|
||||
|
||||
exchange_native_currency = instance.exchange_rates.currency
|
||||
|
||||
for currency in desired_currencies:
|
||||
if currency not in provided_currencies:
|
||||
_LOGGER.warning(
|
||||
"The currency %s is no longer provided by your account, please check "
|
||||
"your settings in Coinbase's developer tools",
|
||||
currency,
|
||||
)
|
||||
break
|
||||
entities.append(AccountSensor(instance, currency))
|
||||
|
||||
if CONF_EXCHANGE_RATES in config_entry.options:
|
||||
for rate in config_entry.options[CONF_EXCHANGE_RATES]:
|
||||
entities.append(
|
||||
ExchangeRateSensor(
|
||||
instance,
|
||||
rate,
|
||||
exchange_native_currency,
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class AccountSensor(SensorEntity):
|
||||
"""Representation of a Coinbase.com sensor."""
|
||||
|
||||
def __init__(self, coinbase_data, name, currency):
|
||||
def __init__(self, coinbase_data, currency):
|
||||
"""Initialize the sensor."""
|
||||
self._coinbase_data = coinbase_data
|
||||
self._name = f"Coinbase {name}"
|
||||
self._state = None
|
||||
self._unit_of_measurement = currency
|
||||
self._native_balance = None
|
||||
self._native_currency = None
|
||||
self._currency = currency
|
||||
for account in coinbase_data.accounts:
|
||||
if account.currency == currency:
|
||||
self._name = f"Coinbase {account[API_ACCOUNT_NAME]}"
|
||||
self._id = f"coinbase-{account[API_ACCOUNT_ID]}"
|
||||
self._state = account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT]
|
||||
self._unit_of_measurement = account[API_ACCOUNT_CURRENCY]
|
||||
self._native_balance = account[API_ACCOUNT_NATIVE_BALANCE][
|
||||
API_ACCOUNT_AMOUNT
|
||||
]
|
||||
self._native_currency = account[API_ACCOUNT_NATIVE_BALANCE][
|
||||
API_ACCOUNT_CURRENCY
|
||||
]
|
||||
break
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the Unique ID of the sensor."""
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
|
@ -82,10 +132,15 @@ class AccountSensor(SensorEntity):
|
|||
"""Get the latest state of the sensor."""
|
||||
self._coinbase_data.update()
|
||||
for account in self._coinbase_data.accounts:
|
||||
if self._name == f"Coinbase {account['name']}":
|
||||
self._state = account["balance"]["amount"]
|
||||
self._native_balance = account["native_balance"]["amount"]
|
||||
self._native_currency = account["native_balance"]["currency"]
|
||||
if account.currency == self._currency:
|
||||
self._state = account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT]
|
||||
self._native_balance = account[API_ACCOUNT_NATIVE_BALANCE][
|
||||
API_ACCOUNT_AMOUNT
|
||||
]
|
||||
self._native_currency = account[API_ACCOUNT_NATIVE_BALANCE][
|
||||
API_ACCOUNT_CURRENCY
|
||||
]
|
||||
break
|
||||
|
||||
|
||||
class ExchangeRateSensor(SensorEntity):
|
||||
|
@ -96,7 +151,10 @@ class ExchangeRateSensor(SensorEntity):
|
|||
self._coinbase_data = coinbase_data
|
||||
self.currency = exchange_currency
|
||||
self._name = f"{exchange_currency} Exchange Rate"
|
||||
self._state = None
|
||||
self._id = f"{coinbase_data.user_id}-xe-{exchange_currency}"
|
||||
self._state = round(
|
||||
1 / float(self._coinbase_data.exchange_rates.rates[self.currency]), 2
|
||||
)
|
||||
self._unit_of_measurement = native_currency
|
||||
|
||||
@property
|
||||
|
@ -104,6 +162,11 @@ class ExchangeRateSensor(SensorEntity):
|
|||
"""Return the name of the sensor."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique ID of the sensor."""
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the sensor."""
|
||||
|
@ -127,5 +190,6 @@ class ExchangeRateSensor(SensorEntity):
|
|||
def update(self):
|
||||
"""Get the latest state of the sensor."""
|
||||
self._coinbase_data.update()
|
||||
rate = self._coinbase_data.exchange_rates.rates[self.currency]
|
||||
self._state = round(1 / float(rate), 2)
|
||||
self._state = round(
|
||||
1 / float(self._coinbase_data.exchange_rates.rates[self.currency]), 2
|
||||
)
|
||||
|
|
40
homeassistant/components/coinbase/strings.json
Normal file
40
homeassistant/components/coinbase/strings.json
Normal file
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Coinbase API Key Details",
|
||||
"description": "Please enter the details of your API key as provided by Coinbase. Separate multiple currencies with a comma (e.g., \"BTC, EUR\")",
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"api_token": "API Secret",
|
||||
"currencies": "Account Balance Currencies",
|
||||
"exchange_rates": "Exchange Rates"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "Adjust Coinbase Options",
|
||||
"data": {
|
||||
"account_balance_currencies": "Wallet balances to report.",
|
||||
"exchange_rate_currencies": "Exchange rates to report."
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||
"currency_unavaliable": "One or more of the requested currency balances is not provided by your Coinbase API.",
|
||||
"exchange_rate_unavaliable": "One or more of the requested exchange rates is not provided by Coinbase."
|
||||
}
|
||||
}
|
||||
}
|
40
homeassistant/components/coinbase/translations/en.json
Normal file
40
homeassistant/components/coinbase/translations/en.json
Normal file
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect",
|
||||
"invalid_auth": "Invalid authentication",
|
||||
"unknown": "Unexpected error"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "API Key",
|
||||
"api_token": "API Secret",
|
||||
"currencies": "Account Balance Currencies",
|
||||
"exchange_rates": "Exchange Rates"
|
||||
},
|
||||
"description": "Please enter the details of your API key as provided by Coinbase. Separate multiple currencies with a comma (e.g., \"BTC, EUR\")",
|
||||
"title": "Coinbase API Key Details"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"error": {
|
||||
"currency_unavaliable": "One or more of the requested currency balances is not provided by your Coinbase API.",
|
||||
"exchange_rate_unavaliable": "One or more of the requested exchange rates is not provided by Coinbase.",
|
||||
"unknown": "Unexpected error"
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"account_balance_currencies": "Wallet balances to report.",
|
||||
"exchange_rate_currencies": "Exchange rates to report."
|
||||
},
|
||||
"description": "Adjust Coinbase Options"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -45,6 +45,7 @@ FLOWS = [
|
|||
"cert_expiry",
|
||||
"climacell",
|
||||
"cloudflare",
|
||||
"coinbase",
|
||||
"control4",
|
||||
"coolmaster",
|
||||
"coronavirus",
|
||||
|
|
|
@ -249,6 +249,9 @@ buienradar==1.0.4
|
|||
# homeassistant.components.caldav
|
||||
caldav==0.7.1
|
||||
|
||||
# homeassistant.components.coinbase
|
||||
coinbase==2.1.0
|
||||
|
||||
# homeassistant.scripts.check_config
|
||||
colorlog==5.0.1
|
||||
|
||||
|
|
1
tests/components/coinbase/__init__.py
Normal file
1
tests/components/coinbase/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
"""Tests for the Coinbase integration."""
|
84
tests/components/coinbase/common.py
Normal file
84
tests/components/coinbase/common.py
Normal file
|
@ -0,0 +1,84 @@
|
|||
"""Collection of helpers."""
|
||||
from homeassistant.components.coinbase.const import (
|
||||
CONF_CURRENCIES,
|
||||
CONF_EXCHANGE_RATES,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN
|
||||
|
||||
from .const import GOOD_EXCHNAGE_RATE, GOOD_EXCHNAGE_RATE_2, MOCK_ACCOUNTS_RESPONSE
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
class MockPagination:
|
||||
"""Mock pagination result."""
|
||||
|
||||
def __init__(self, value=None):
|
||||
"""Load simple pagination for tests."""
|
||||
self.next_starting_after = value
|
||||
|
||||
|
||||
class MockGetAccounts:
|
||||
"""Mock accounts with pagination."""
|
||||
|
||||
def __init__(self, starting_after=0):
|
||||
"""Init mocked object, forced to return two at a time."""
|
||||
if (target_end := starting_after + 2) >= (
|
||||
max_end := len(MOCK_ACCOUNTS_RESPONSE)
|
||||
):
|
||||
end = max_end
|
||||
self.pagination = MockPagination(value=None)
|
||||
else:
|
||||
end = target_end
|
||||
self.pagination = MockPagination(value=target_end)
|
||||
|
||||
self.accounts = {
|
||||
"data": MOCK_ACCOUNTS_RESPONSE[starting_after:end],
|
||||
}
|
||||
self.started_at = starting_after
|
||||
|
||||
def __getitem__(self, item):
|
||||
"""Handle subscript request."""
|
||||
return self.accounts[item]
|
||||
|
||||
|
||||
def mocked_get_accounts(_, **kwargs):
|
||||
"""Return simplied accounts using mock."""
|
||||
return MockGetAccounts(**kwargs)
|
||||
|
||||
|
||||
def mock_get_current_user():
|
||||
"""Return a simplified mock user."""
|
||||
return {
|
||||
"id": "123456-abcdef",
|
||||
"name": "Test User",
|
||||
}
|
||||
|
||||
|
||||
def mock_get_exchange_rates():
|
||||
"""Return a heavily reduced mock list of exchange rates for testing."""
|
||||
return {
|
||||
"currency": "USD",
|
||||
"rates": {GOOD_EXCHNAGE_RATE_2: "0.109", GOOD_EXCHNAGE_RATE: "0.00002"},
|
||||
}
|
||||
|
||||
|
||||
async def init_mock_coinbase(hass):
|
||||
"""Init Coinbase integration for testing."""
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id="abcde12345",
|
||||
title="Test User",
|
||||
data={CONF_API_KEY: "123456", CONF_API_TOKEN: "AbCDeF"},
|
||||
options={
|
||||
CONF_CURRENCIES: [],
|
||||
CONF_EXCHANGE_RATES: [],
|
||||
},
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return config_entry
|
33
tests/components/coinbase/const.py
Normal file
33
tests/components/coinbase/const.py
Normal file
|
@ -0,0 +1,33 @@
|
|||
"""Constants for testing the Coinbase integration."""
|
||||
|
||||
GOOD_CURRENCY = "BTC"
|
||||
GOOD_CURRENCY_2 = "USD"
|
||||
GOOD_CURRENCY_3 = "EUR"
|
||||
GOOD_EXCHNAGE_RATE = "BTC"
|
||||
GOOD_EXCHNAGE_RATE_2 = "ATOM"
|
||||
BAD_CURRENCY = "ETH"
|
||||
BAD_EXCHANGE_RATE = "ETH"
|
||||
|
||||
MOCK_ACCOUNTS_RESPONSE = [
|
||||
{
|
||||
"balance": {"amount": "13.38", "currency": GOOD_CURRENCY_3},
|
||||
"currency": "BTC",
|
||||
"id": "ABCDEF",
|
||||
"name": "BTC Wallet",
|
||||
"native_balance": {"amount": "15.02", "currency": GOOD_CURRENCY_2},
|
||||
},
|
||||
{
|
||||
"balance": {"amount": "0.00001", "currency": GOOD_CURRENCY},
|
||||
"currency": "BTC",
|
||||
"id": "123456789",
|
||||
"name": "BTC Wallet",
|
||||
"native_balance": {"amount": "100.12", "currency": GOOD_CURRENCY_2},
|
||||
},
|
||||
{
|
||||
"balance": {"amount": "9.90", "currency": GOOD_CURRENCY_2},
|
||||
"currency": "USD",
|
||||
"id": "987654321",
|
||||
"name": "USD Wallet",
|
||||
"native_balance": {"amount": "9.90", "currency": GOOD_CURRENCY_2},
|
||||
},
|
||||
]
|
332
tests/components/coinbase/test_config_flow.py
Normal file
332
tests/components/coinbase/test_config_flow.py
Normal file
|
@ -0,0 +1,332 @@
|
|||
"""Test the Coinbase config flow."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from coinbase.wallet.error import AuthenticationError
|
||||
from requests.models import Response
|
||||
|
||||
from homeassistant import config_entries, setup
|
||||
from homeassistant.components.coinbase.const import (
|
||||
CONF_CURRENCIES,
|
||||
CONF_EXCHANGE_RATES,
|
||||
CONF_YAML_API_TOKEN,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN
|
||||
|
||||
from .common import (
|
||||
init_mock_coinbase,
|
||||
mock_get_current_user,
|
||||
mock_get_exchange_rates,
|
||||
mocked_get_accounts,
|
||||
)
|
||||
from .const import BAD_CURRENCY, BAD_EXCHANGE_RATE, GOOD_CURRENCY, GOOD_EXCHNAGE_RATE
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_form(hass):
|
||||
"""Test we get the form."""
|
||||
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == "form"
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch(
|
||||
"coinbase.wallet.client.Client.get_current_user",
|
||||
return_value=mock_get_current_user(),
|
||||
), patch(
|
||||
"coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts
|
||||
), patch(
|
||||
"coinbase.wallet.client.Client.get_exchange_rates",
|
||||
return_value=mock_get_exchange_rates(),
|
||||
), patch(
|
||||
"homeassistant.components.coinbase.async_setup", return_value=True
|
||||
) as mock_setup, patch(
|
||||
"homeassistant.components.coinbase.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_API_KEY: "123456",
|
||||
CONF_API_TOKEN: "AbCDeF",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == "create_entry"
|
||||
assert result2["title"] == "Test User"
|
||||
assert result2["data"] == {CONF_API_KEY: "123456", CONF_API_TOKEN: "AbCDeF"}
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_form_invalid_auth(hass):
|
||||
"""Test we handle invalid auth."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
response = Response()
|
||||
response.status_code = 401
|
||||
api_auth_error = AuthenticationError(
|
||||
response,
|
||||
"authentication_error",
|
||||
"invalid signature",
|
||||
[{"id": "authentication_error", "message": "invalid signature"}],
|
||||
)
|
||||
with patch(
|
||||
"coinbase.wallet.client.Client.get_current_user",
|
||||
side_effect=api_auth_error,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_API_KEY: "123456",
|
||||
CONF_API_TOKEN: "AbCDeF",
|
||||
},
|
||||
)
|
||||
|
||||
assert result2["type"] == "form"
|
||||
assert result2["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
|
||||
async def test_form_cannot_connect(hass):
|
||||
"""Test we handle cannot connect error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"coinbase.wallet.client.Client.get_current_user",
|
||||
side_effect=ConnectionError,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_API_KEY: "123456",
|
||||
CONF_API_TOKEN: "AbCDeF",
|
||||
},
|
||||
)
|
||||
|
||||
assert result2["type"] == "form"
|
||||
assert result2["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_form_catch_all_exception(hass):
|
||||
"""Test we handle unknown exceptions."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"coinbase.wallet.client.Client.get_current_user",
|
||||
side_effect=Exception,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_API_KEY: "123456",
|
||||
CONF_API_TOKEN: "AbCDeF",
|
||||
},
|
||||
)
|
||||
|
||||
assert result2["type"] == "form"
|
||||
assert result2["errors"] == {"base": "unknown"}
|
||||
|
||||
|
||||
async def test_option_good_account_currency(hass):
|
||||
"""Test we handle a good wallet currency option."""
|
||||
with patch(
|
||||
"coinbase.wallet.client.Client.get_current_user",
|
||||
return_value=mock_get_current_user(),
|
||||
), patch(
|
||||
"coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts
|
||||
), patch(
|
||||
"coinbase.wallet.client.Client.get_exchange_rates",
|
||||
return_value=mock_get_exchange_rates(),
|
||||
):
|
||||
config_entry = await init_mock_coinbase(hass)
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
result2 = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_CURRENCIES: [GOOD_CURRENCY],
|
||||
CONF_EXCHANGE_RATES: [],
|
||||
},
|
||||
)
|
||||
assert result2["type"] == "create_entry"
|
||||
|
||||
|
||||
async def test_form_bad_account_currency(hass):
|
||||
"""Test we handle a bad currency option."""
|
||||
with patch(
|
||||
"coinbase.wallet.client.Client.get_current_user",
|
||||
return_value=mock_get_current_user(),
|
||||
), patch(
|
||||
"coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts
|
||||
), patch(
|
||||
"coinbase.wallet.client.Client.get_exchange_rates",
|
||||
return_value=mock_get_exchange_rates(),
|
||||
):
|
||||
config_entry = await init_mock_coinbase(hass)
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
result2 = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_CURRENCIES: [BAD_CURRENCY],
|
||||
CONF_EXCHANGE_RATES: [],
|
||||
},
|
||||
)
|
||||
|
||||
assert result2["type"] == "form"
|
||||
assert result2["errors"] == {"base": "currency_unavaliable"}
|
||||
|
||||
|
||||
async def test_option_good_exchange_rate(hass):
|
||||
"""Test we handle a good exchange rate option."""
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id="abcde12345",
|
||||
title="Test User",
|
||||
data={CONF_API_KEY: "123456", CONF_API_TOKEN: "AbCDeF"},
|
||||
options={
|
||||
CONF_CURRENCIES: [],
|
||||
CONF_EXCHANGE_RATES: [],
|
||||
},
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"coinbase.wallet.client.Client.get_current_user",
|
||||
return_value=mock_get_current_user(),
|
||||
), patch(
|
||||
"coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts
|
||||
), patch(
|
||||
"coinbase.wallet.client.Client.get_exchange_rates",
|
||||
return_value=mock_get_exchange_rates(),
|
||||
):
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
result2 = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_CURRENCIES: [],
|
||||
CONF_EXCHANGE_RATES: [GOOD_EXCHNAGE_RATE],
|
||||
},
|
||||
)
|
||||
assert result2["type"] == "create_entry"
|
||||
|
||||
|
||||
async def test_form_bad_exchange_rate(hass):
|
||||
"""Test we handle a bad exchange rate."""
|
||||
with patch(
|
||||
"coinbase.wallet.client.Client.get_current_user",
|
||||
return_value=mock_get_current_user(),
|
||||
), patch(
|
||||
"coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts
|
||||
), patch(
|
||||
"coinbase.wallet.client.Client.get_exchange_rates",
|
||||
return_value=mock_get_exchange_rates(),
|
||||
):
|
||||
config_entry = await init_mock_coinbase(hass)
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
result2 = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_CURRENCIES: [],
|
||||
CONF_EXCHANGE_RATES: [BAD_EXCHANGE_RATE],
|
||||
},
|
||||
)
|
||||
assert result2["type"] == "form"
|
||||
assert result2["errors"] == {"base": "exchange_rate_unavaliable"}
|
||||
|
||||
|
||||
async def test_option_catch_all_exception(hass):
|
||||
"""Test we handle an unknown exception in the option flow."""
|
||||
with patch(
|
||||
"coinbase.wallet.client.Client.get_current_user",
|
||||
return_value=mock_get_current_user(),
|
||||
), patch(
|
||||
"coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts
|
||||
), patch(
|
||||
"coinbase.wallet.client.Client.get_exchange_rates",
|
||||
return_value=mock_get_exchange_rates(),
|
||||
):
|
||||
config_entry = await init_mock_coinbase(hass)
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with patch(
|
||||
"coinbase.wallet.client.Client.get_accounts",
|
||||
side_effect=Exception,
|
||||
):
|
||||
result2 = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_CURRENCIES: [],
|
||||
CONF_EXCHANGE_RATES: ["ETH"],
|
||||
},
|
||||
)
|
||||
|
||||
assert result2["type"] == "form"
|
||||
assert result2["errors"] == {"base": "unknown"}
|
||||
|
||||
|
||||
async def test_yaml_import(hass):
|
||||
"""Test YAML import works."""
|
||||
conf = {
|
||||
CONF_API_KEY: "123456",
|
||||
CONF_YAML_API_TOKEN: "AbCDeF",
|
||||
CONF_CURRENCIES: ["BTC", "USD"],
|
||||
CONF_EXCHANGE_RATES: ["ATOM", "BTC"],
|
||||
}
|
||||
with patch(
|
||||
"coinbase.wallet.client.Client.get_current_user",
|
||||
return_value=mock_get_current_user(),
|
||||
), patch(
|
||||
"coinbase.wallet.client.Client.get_accounts", new=mocked_get_accounts
|
||||
), patch(
|
||||
"coinbase.wallet.client.Client.get_exchange_rates",
|
||||
return_value=mock_get_exchange_rates(),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=conf
|
||||
)
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["title"] == "Test User"
|
||||
assert result["data"] == {CONF_API_KEY: "123456", CONF_API_TOKEN: "AbCDeF"}
|
||||
assert result["options"] == {
|
||||
CONF_CURRENCIES: ["BTC", "USD"],
|
||||
CONF_EXCHANGE_RATES: ["ATOM", "BTC"],
|
||||
}
|
||||
|
||||
|
||||
async def test_yaml_existing(hass):
|
||||
"""Test YAML ignored when already processed."""
|
||||
MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_API_KEY: "123456",
|
||||
CONF_API_TOKEN: "AbCDeF",
|
||||
},
|
||||
).add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_IMPORT},
|
||||
data={
|
||||
CONF_API_KEY: "123456",
|
||||
CONF_YAML_API_TOKEN: "AbCDeF",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == "abort"
|
||||
assert result["reason"] == "already_configured"
|
80
tests/components/coinbase/test_init.py
Normal file
80
tests/components/coinbase/test_init.py
Normal file
|
@ -0,0 +1,80 @@
|
|||
"""Test the Coinbase integration."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.coinbase.const import (
|
||||
CONF_CURRENCIES,
|
||||
CONF_EXCHANGE_RATES,
|
||||
CONF_YAML_API_TOKEN,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .common import (
|
||||
init_mock_coinbase,
|
||||
mock_get_current_user,
|
||||
mock_get_exchange_rates,
|
||||
mocked_get_accounts,
|
||||
)
|
||||
from .const import (
|
||||
GOOD_CURRENCY,
|
||||
GOOD_CURRENCY_2,
|
||||
GOOD_EXCHNAGE_RATE,
|
||||
GOOD_EXCHNAGE_RATE_2,
|
||||
)
|
||||
|
||||
|
||||
async def test_setup(hass):
|
||||
"""Test setting up from configuration.yaml."""
|
||||
conf = {
|
||||
DOMAIN: {
|
||||
CONF_API_KEY: "123456",
|
||||
CONF_YAML_API_TOKEN: "AbCDeF",
|
||||
CONF_CURRENCIES: [GOOD_CURRENCY, GOOD_CURRENCY_2],
|
||||
CONF_EXCHANGE_RATES: [GOOD_EXCHNAGE_RATE, GOOD_EXCHNAGE_RATE_2],
|
||||
}
|
||||
}
|
||||
with patch(
|
||||
"coinbase.wallet.client.Client.get_current_user",
|
||||
return_value=mock_get_current_user(),
|
||||
), patch(
|
||||
"coinbase.wallet.client.Client.get_accounts",
|
||||
new=mocked_get_accounts,
|
||||
), patch(
|
||||
"coinbase.wallet.client.Client.get_exchange_rates",
|
||||
return_value=mock_get_exchange_rates(),
|
||||
):
|
||||
assert await async_setup_component(hass, DOMAIN, conf)
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
assert len(entries) == 1
|
||||
assert entries[0].title == "Test User"
|
||||
assert entries[0].source == config_entries.SOURCE_IMPORT
|
||||
assert entries[0].options == {
|
||||
CONF_CURRENCIES: [GOOD_CURRENCY, GOOD_CURRENCY_2],
|
||||
CONF_EXCHANGE_RATES: [GOOD_EXCHNAGE_RATE, GOOD_EXCHNAGE_RATE_2],
|
||||
}
|
||||
|
||||
|
||||
async def test_unload_entry(hass):
|
||||
"""Test successful unload of entry."""
|
||||
with patch(
|
||||
"coinbase.wallet.client.Client.get_current_user",
|
||||
return_value=mock_get_current_user(),
|
||||
), patch(
|
||||
"coinbase.wallet.client.Client.get_accounts",
|
||||
new=mocked_get_accounts,
|
||||
), patch(
|
||||
"coinbase.wallet.client.Client.get_exchange_rates",
|
||||
return_value=mock_get_exchange_rates(),
|
||||
):
|
||||
entry = await init_mock_coinbase(hass)
|
||||
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
assert entry.state == config_entries.ConfigEntryState.LOADED
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entry.state == config_entries.ConfigEntryState.NOT_LOADED
|
||||
assert not hass.data.get(DOMAIN)
|
Loading…
Add table
Add a link
Reference in a new issue