mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 17:27:52 +00:00
Add SimpleFIN integration (#108336)
* reset to latest dev branch * Update homeassistant/components/simplefin/sensor.py Co-authored-by: Erik Montnemery <erik@montnemery.com> * formatting tweak * Removed info errors * fix bad billing error message * addressing PR * addressing PR * reauth abort and already_confiugred added to strings.json * adding the reauth message * ruff * update reqs * reset to latest dev branch * Update homeassistant/components/simplefin/sensor.py Co-authored-by: Erik Montnemery <erik@montnemery.com> * formatting tweak * Removed info errors * fix bad billing error message * addressing PR * addressing PR * reauth abort and already_confiugred added to strings.json * adding the reauth message * ruff * update reqs * Update homeassistant/components/simplefin/__init__.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * Addressing a few PR comments - removing nix - and adding runtime data * updated comments * rename config flow * pulling reauth :( - inline stuff * inline more * fixed a tab issue * reverting changes * various PR updates and code removal * generator async add * Update homeassistant/components/simplefin/__init__.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * Update homeassistant/components/simplefin/config_flow.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * Update homeassistant/components/simplefin/config_flow.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * Update homeassistant/components/simplefin/sensor.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * Update homeassistant/components/simplefin/sensor.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * always callable * Update homeassistant/components/simplefin/coordinator.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * no-verify * type * fixing missing domain * it looks like this file is gone now * typing * sorta pass * fix license * Update homeassistant/components/simplefin/config_flow.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * Update homeassistant/components/simplefin/entity.py Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * addressing PR * Update homeassistant/components/simplefin/strings.json Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com> * move property to entity.py * moved stuff out to else block * Initial Snappshot Testing ... still have unadressed changes to make * Addressing PR Comments * pushing back to joost * removing non-needed file * added more asserts * reducing mocks - need to fix patch paths still * Changed patch to be more localized to config_flow * Removed unneeded fixture * Moved coordinator around * Cleaning up the code * Removed a NOQA" * Upping the number of asserts * cleanup * fixed abort call * incremental update - for Josot... changed a function signature and removed an annotatoin * no-verify * Added an abort test * ruff * increased coverage but it might not pass muster for JOOST * increased coverage but it might not pass muster for JOOST * Much nicer test now * tried to simplify * Fix nits --------- Co-authored-by: Erik Montnemery <erik@montnemery.com> Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
parent
42dcd693d5
commit
0213f1c5c0
@ -1275,6 +1275,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/sighthound/ @robmarkcole
|
||||
/homeassistant/components/signal_messenger/ @bbernhard
|
||||
/tests/components/signal_messenger/ @bbernhard
|
||||
/homeassistant/components/simplefin/ @scottg489 @jeeftor
|
||||
/tests/components/simplefin/ @scottg489 @jeeftor
|
||||
/homeassistant/components/simplepush/ @engrbm87
|
||||
/tests/components/simplepush/ @engrbm87
|
||||
/homeassistant/components/simplisafe/ @bachya
|
||||
|
33
homeassistant/components/simplefin/__init__.py
Normal file
33
homeassistant/components/simplefin/__init__.py
Normal file
@ -0,0 +1,33 @@
|
||||
"""The SimpleFIN integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from simplefin4py import SimpleFin
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import CONF_ACCESS_URL
|
||||
from .coordinator import SimpleFinDataUpdateCoordinator
|
||||
|
||||
PLATFORMS: list[str] = [Platform.SENSOR]
|
||||
|
||||
|
||||
type SimpleFinConfigEntry = ConfigEntry[SimpleFinDataUpdateCoordinator]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: SimpleFinConfigEntry) -> bool:
|
||||
"""Set up from a config entry."""
|
||||
access_url = entry.data[CONF_ACCESS_URL]
|
||||
sf_client = SimpleFin(access_url)
|
||||
sf_coordinator = SimpleFinDataUpdateCoordinator(hass, sf_client)
|
||||
await sf_coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data = sf_coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
75
homeassistant/components/simplefin/config_flow.py
Normal file
75
homeassistant/components/simplefin/config_flow.py
Normal file
@ -0,0 +1,75 @@
|
||||
"""Config flow for SimpleFIN integration."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from simplefin4py import SimpleFin
|
||||
from simplefin4py.exceptions import (
|
||||
SimpleFinAuthError,
|
||||
SimpleFinClaimError,
|
||||
SimpleFinInvalidAccountURLError,
|
||||
SimpleFinInvalidClaimTokenError,
|
||||
SimpleFinPaymentRequiredError,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
|
||||
from .const import CONF_ACCESS_URL, DOMAIN, LOGGER
|
||||
|
||||
|
||||
class SimpleFinConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for the initial setup of a SimpleFIN integration."""
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Prompt user for SimpleFIN API credentials."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
access_url: str = user_input[CONF_ACCESS_URL]
|
||||
self._async_abort_entries_match({CONF_ACCESS_URL: access_url})
|
||||
|
||||
try:
|
||||
if not access_url.startswith("http"):
|
||||
# Claim token detected - convert to access url
|
||||
LOGGER.debug("[Setup Token] - Claiming Access URL")
|
||||
access_url = await SimpleFin.claim_setup_token(access_url)
|
||||
else:
|
||||
LOGGER.debug("[Access Url] - 'http' string detected")
|
||||
# Validate the access URL
|
||||
LOGGER.debug("[Access Url] - validating access url")
|
||||
SimpleFin.decode_access_url(access_url)
|
||||
|
||||
LOGGER.debug("[Access Url] - Fetching data")
|
||||
simple_fin = SimpleFin(access_url=access_url)
|
||||
await simple_fin.fetch_data()
|
||||
|
||||
except SimpleFinInvalidAccountURLError:
|
||||
errors["base"] = "url_error"
|
||||
except SimpleFinInvalidClaimTokenError:
|
||||
errors["base"] = "invalid_claim_token"
|
||||
except SimpleFinClaimError:
|
||||
errors["base"] = "claim_error"
|
||||
except SimpleFinPaymentRequiredError:
|
||||
errors["base"] = "payment_required"
|
||||
except SimpleFinAuthError:
|
||||
errors["base"] = "invalid_auth"
|
||||
else:
|
||||
# We passed validation
|
||||
user_input[CONF_ACCESS_URL] = access_url
|
||||
|
||||
return self.async_create_entry(
|
||||
title="SimpleFIN",
|
||||
data={CONF_ACCESS_URL: user_input[CONF_ACCESS_URL]},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ACCESS_URL): str,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
12
homeassistant/components/simplefin/const.py
Normal file
12
homeassistant/components/simplefin/const.py
Normal file
@ -0,0 +1,12 @@
|
||||
"""Constants for the SimpleFIN integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Final
|
||||
|
||||
DOMAIN: Final = "simplefin"
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
CONF_ACCESS_URL = "access_url"
|
45
homeassistant/components/simplefin/coordinator.py
Normal file
45
homeassistant/components/simplefin/coordinator.py
Normal file
@ -0,0 +1,45 @@
|
||||
"""Data update coordinator for the SimpleFIN integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
from simplefin4py import FinancialData, SimpleFin
|
||||
from simplefin4py.exceptions import SimpleFinAuthError, SimpleFinPaymentRequiredError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import LOGGER
|
||||
|
||||
|
||||
class SimpleFinDataUpdateCoordinator(DataUpdateCoordinator[FinancialData]):
|
||||
"""Data update coordinator for the SimpleFIN integration."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, client: SimpleFin) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass=hass,
|
||||
logger=LOGGER,
|
||||
name="simplefin",
|
||||
update_interval=timedelta(hours=4),
|
||||
)
|
||||
self.client = client
|
||||
|
||||
async def _async_update_data(self) -> Any:
|
||||
"""Fetch data for all accounts."""
|
||||
try:
|
||||
return await self.client.fetch_data()
|
||||
except SimpleFinAuthError as err:
|
||||
raise ConfigEntryError("Authentication failed") from err
|
||||
|
||||
except SimpleFinPaymentRequiredError as err:
|
||||
LOGGER.warning(
|
||||
"There is a billing issue with your SimpleFin account, contact Simplefin to address this issue"
|
||||
)
|
||||
raise UpdateFailed from err
|
43
homeassistant/components/simplefin/entity.py
Normal file
43
homeassistant/components/simplefin/entity.py
Normal file
@ -0,0 +1,43 @@
|
||||
"""SimpleFin Base Entity."""
|
||||
|
||||
from simplefin4py import Account
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import SimpleFinDataUpdateCoordinator
|
||||
|
||||
|
||||
class SimpleFinEntity(CoordinatorEntity[SimpleFinDataUpdateCoordinator]):
|
||||
"""Define a generic class for SimpleFIN entities."""
|
||||
|
||||
_attr_attribution = "Data provided by SimpleFIN API"
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: SimpleFinDataUpdateCoordinator,
|
||||
description: EntityDescription,
|
||||
account: Account,
|
||||
) -> None:
|
||||
"""Class initializer."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._account_id = account.id
|
||||
|
||||
self._attr_unique_id = f"account_{account.id}_{description.key}"
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, account.id)},
|
||||
name=f"{account.org.name} {account.name}",
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
manufacturer=account.org.name,
|
||||
model=account.name,
|
||||
)
|
||||
|
||||
@property
|
||||
def account_data(self) -> Account:
|
||||
"""Return the account data."""
|
||||
return self.coordinator.data.get_account_for_id(self._account_id)
|
11
homeassistant/components/simplefin/manifest.json
Normal file
11
homeassistant/components/simplefin/manifest.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"domain": "simplefin",
|
||||
"name": "SimpleFin",
|
||||
"codeowners": ["@scottg489", "@jeeftor"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/simplefin",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["simplefin"],
|
||||
"requirements": ["simplefin4py==0.0.16"]
|
||||
}
|
91
homeassistant/components/simplefin/sensor.py
Normal file
91
homeassistant/components/simplefin/sensor.py
Normal file
@ -0,0 +1,91 @@
|
||||
"""Platform for sensor integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from simplefin4py import Account
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from . import SimpleFinConfigEntry
|
||||
from .entity import SimpleFinEntity
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class SimpleFinSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes a sensor entity."""
|
||||
|
||||
value_fn: Callable[[Account], StateType]
|
||||
icon_fn: Callable[[Account], str] | None = None
|
||||
unit_fn: Callable[[Account], str] | None = None
|
||||
|
||||
|
||||
SIMPLEFIN_SENSORS: tuple[SimpleFinSensorEntityDescription, ...] = (
|
||||
SimpleFinSensorEntityDescription(
|
||||
key="balance",
|
||||
translation_key="balance",
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
device_class=SensorDeviceClass.MONETARY,
|
||||
value_fn=lambda account: account.balance,
|
||||
unit_fn=lambda account: account.currency,
|
||||
icon_fn=lambda account: account.inferred_account_type,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: SimpleFinConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up SimpleFIN sensors for config entries."""
|
||||
|
||||
sf_coordinator = config_entry.runtime_data
|
||||
accounts = sf_coordinator.data.accounts
|
||||
|
||||
async_add_entities(
|
||||
SimpleFinSensor(
|
||||
sf_coordinator,
|
||||
sensor_description,
|
||||
account,
|
||||
)
|
||||
for account in accounts
|
||||
for sensor_description in SIMPLEFIN_SENSORS
|
||||
)
|
||||
|
||||
|
||||
class SimpleFinSensor(SimpleFinEntity, SensorEntity):
|
||||
"""Defines a SimpleFIN sensor."""
|
||||
|
||||
entity_description: SimpleFinSensorEntityDescription
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state."""
|
||||
return self.entity_description.value_fn(self.account_data)
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
"""Return the icon of this account."""
|
||||
|
||||
if self.entity_description.icon_fn is not None:
|
||||
return self.entity_description.icon_fn(self.account_data)
|
||||
return None
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return the currency of this account."""
|
||||
if self.entity_description.unit_fn:
|
||||
return self.entity_description.unit_fn(self.account_data)
|
||||
|
||||
return None
|
30
homeassistant/components/simplefin/strings.json
Normal file
30
homeassistant/components/simplefin/strings.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Please enter either a Claim Token or an Access URL.",
|
||||
"data": {
|
||||
"api_token": "Claim Token or Access URL"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_auth": "Authentication failed: This could be due to revoked access or incorrect credentials",
|
||||
"claim_error": "The claim token either does not exist or has already been used claimed by someone else. Receiving this could mean that the user\u2019s transaction information has been compromised",
|
||||
"invalid_claim_token": "The claim token is invalid and could not be decoded",
|
||||
"payment_required": "You presented a valid access url, however payment is required before you can obtain data",
|
||||
"url_error": "There was an issue parsing the Account URL"
|
||||
},
|
||||
"abort": {
|
||||
"missing_access_url": "Access URL or Claim Token missing",
|
||||
"already_configured": "This Access URL is already configured."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"balance": {
|
||||
"name": "Balance"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -499,6 +499,7 @@ FLOWS = {
|
||||
"shelly",
|
||||
"shopping_list",
|
||||
"sia",
|
||||
"simplefin",
|
||||
"simplepush",
|
||||
"simplisafe",
|
||||
"skybell",
|
||||
|
@ -5432,6 +5432,12 @@
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_push"
|
||||
},
|
||||
"simplefin": {
|
||||
"name": "SimpleFin",
|
||||
"integration_type": "service",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"simplepush": {
|
||||
"name": "Simplepush",
|
||||
"integration_type": "hub",
|
||||
|
@ -2567,6 +2567,9 @@ sharp_aquos_rc==0.3.2
|
||||
# homeassistant.components.shodan
|
||||
shodan==1.28.0
|
||||
|
||||
# homeassistant.components.simplefin
|
||||
simplefin4py==0.0.16
|
||||
|
||||
# homeassistant.components.sighthound
|
||||
simplehound==0.3
|
||||
|
||||
|
@ -1995,6 +1995,9 @@ sfrbox-api==0.0.8
|
||||
# homeassistant.components.sharkiq
|
||||
sharkiq==1.0.2
|
||||
|
||||
# homeassistant.components.simplefin
|
||||
simplefin4py==0.0.16
|
||||
|
||||
# homeassistant.components.sighthound
|
||||
simplehound==0.3
|
||||
|
||||
|
13
tests/components/simplefin/__init__.py
Normal file
13
tests/components/simplefin/__init__.py
Normal file
@ -0,0 +1,13 @@
|
||||
"""Tests for SimpleFin."""
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
|
||||
"""Fixture for setting up the component."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
83
tests/components/simplefin/conftest.py
Normal file
83
tests/components/simplefin/conftest.py
Normal file
@ -0,0 +1,83 @@
|
||||
"""Test fixtures for SimpleFIN."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from simplefin4py import FinancialData
|
||||
from simplefin4py.exceptions import SimpleFinInvalidClaimTokenError
|
||||
|
||||
from homeassistant.components.simplefin import CONF_ACCESS_URL
|
||||
from homeassistant.components.simplefin.const import DOMAIN
|
||||
|
||||
from tests.common import MockConfigEntry, load_fixture
|
||||
|
||||
MOCK_ACCESS_URL = "https://i:am@yomama.house.com"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
|
||||
"""Mock setting up a config entry."""
|
||||
with patch(
|
||||
"homeassistant.components.simplefin.async_setup_entry", return_value=True
|
||||
) as mock_setup:
|
||||
yield mock_setup
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def mock_config_entry() -> MockConfigEntry:
|
||||
"""Fixture for MockConfigEntry."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={CONF_ACCESS_URL: MOCK_ACCESS_URL},
|
||||
version=1,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_claim_setup_token() -> str:
|
||||
"""Fixture to mock the claim_setup_token method of SimpleFin."""
|
||||
with patch(
|
||||
"homeassistant.components.simplefin.config_flow.SimpleFin.claim_setup_token",
|
||||
) as mock_claim_setup_token:
|
||||
mock_claim_setup_token.return_value = "https://i:am@yomama.comma"
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_decode_claim_token_invalid_then_good() -> str:
|
||||
"""Fixture to mock the decode_claim_token method of SimpleFin."""
|
||||
return_values = [SimpleFinInvalidClaimTokenError, "valid_return_value"]
|
||||
with patch(
|
||||
"homeassistant.components.simplefin.config_flow.SimpleFin.decode_claim_token",
|
||||
new_callable=lambda: MagicMock(side_effect=return_values),
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_simplefin_client() -> Generator[AsyncMock]:
|
||||
"""Mock a SimpleFin client."""
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.simplefin.SimpleFin",
|
||||
autospec=True,
|
||||
) as mock_client,
|
||||
patch(
|
||||
"homeassistant.components.simplefin.config_flow.SimpleFin",
|
||||
new=mock_client,
|
||||
),
|
||||
):
|
||||
mock_client.claim_setup_token.return_value = MOCK_ACCESS_URL
|
||||
client = mock_client.return_value
|
||||
|
||||
fixture_data = load_fixture("fin_data.json", DOMAIN)
|
||||
fin_data = FinancialData.from_json(fixture_data)
|
||||
|
||||
assert fin_data.accounts != []
|
||||
client.fetch_data.return_value = fin_data
|
||||
|
||||
client.access_url = MOCK_ACCESS_URL
|
||||
|
||||
yield mock_client
|
173
tests/components/simplefin/fixtures/fin_data.json
Normal file
173
tests/components/simplefin/fixtures/fin_data.json
Normal file
@ -0,0 +1,173 @@
|
||||
{
|
||||
"errors": [
|
||||
"Connection to Investments may need attention",
|
||||
"Connection to The Bank of Go may need attention"
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
"org": {
|
||||
"domain": "www.newwealthfront.com",
|
||||
"name": "The Bank of Go",
|
||||
"sfin-url": "https://beta-bridge.simplefin.org/simplefin",
|
||||
"url": "https://www.newwealthfront.com"
|
||||
},
|
||||
"id": "ACT-1a2b3c4d-5e6f-7g8h-9i0j",
|
||||
"name": "The Bank",
|
||||
"currency": "USD",
|
||||
"balance": "7777.77",
|
||||
"available-balance": "7777.77",
|
||||
"balance-date": 1705413843,
|
||||
"transactions": [
|
||||
{
|
||||
"id": "12394832938403",
|
||||
"posted": 793090572,
|
||||
"amount": "-1234.56",
|
||||
"description": "Enchanted Bait Shop",
|
||||
"payee": "Uncle Frank",
|
||||
"memo": "Some memo",
|
||||
"transacted_at": 793080572
|
||||
}
|
||||
],
|
||||
"extra": {
|
||||
"account-open-date": 978360153
|
||||
},
|
||||
"holdings": []
|
||||
},
|
||||
{
|
||||
"org": {
|
||||
"domain": "www.newfidelity.com",
|
||||
"name": "Investments",
|
||||
"sfin-url": "https://beta-bridge.simplefin.org/simplefin",
|
||||
"url": "https://www.newfidelity.com"
|
||||
},
|
||||
"id": "ACT-1k2l3m4n-5o6p-7q8r-9s0t",
|
||||
"name": "My Checking",
|
||||
"currency": "USD",
|
||||
"balance": "12345.67",
|
||||
"available-balance": "5432.10",
|
||||
"balance-date": 1705413319,
|
||||
"transactions": [],
|
||||
"holdings": []
|
||||
},
|
||||
{
|
||||
"org": {
|
||||
"domain": "www.newhfcu.org",
|
||||
"name": "The Bank of Go",
|
||||
"sfin-url": "https://beta-bridge.simplefin.org/simplefin",
|
||||
"url": "https://www.newhfcu.org/"
|
||||
},
|
||||
"id": "ACT-2a3b4c5d-6e7f-8g9h-0i1j",
|
||||
"name": "PRIME SAVINGS",
|
||||
"currency": "EUR",
|
||||
"balance": "9876.54",
|
||||
"available-balance": "8765.43",
|
||||
"balance-date": 1705428861,
|
||||
"transactions": [],
|
||||
"holdings": []
|
||||
},
|
||||
{
|
||||
"org": {
|
||||
"domain": "www.randombank2.com",
|
||||
"name": "Random Bank",
|
||||
"sfin-url": "https://beta-bridge.simplefin.org/simplefin",
|
||||
"url": "https://www.randombank2.com/"
|
||||
},
|
||||
"id": "ACT-3a4b5c6d-7e8f-9g0h-1i2j",
|
||||
"name": "Costco Anywhere Visa® Card",
|
||||
"currency": "USD",
|
||||
"balance": "-532.69",
|
||||
"available-balance": "4321.98",
|
||||
"balance-date": 1705429002,
|
||||
"transactions": [],
|
||||
"holdings": []
|
||||
},
|
||||
{
|
||||
"org": {
|
||||
"domain": "www.newfidelity.com",
|
||||
"name": "Investments",
|
||||
"sfin-url": "https://beta-bridge.simplefin.org/simplefin",
|
||||
"url": "https://www.newfidelity.com"
|
||||
},
|
||||
"id": "ACT-4k5l6m7n-8o9p-1q2r-3s4t",
|
||||
"name": "Dr Evil",
|
||||
"currency": "USD",
|
||||
"balance": "1000000.00",
|
||||
"available-balance": "13579.24",
|
||||
"balance-date": 1705413319,
|
||||
"transactions": [],
|
||||
"holdings": [
|
||||
{
|
||||
"id": "HOL-62eb5bb6-4aed-4fe1-bdbe-f28e127e359b",
|
||||
"created": 1705413320,
|
||||
"currency": "",
|
||||
"cost_basis": "10000.00",
|
||||
"description": "Fantastic FID GROWTH CO K6",
|
||||
"market_value": "15000.00",
|
||||
"purchase_price": "0.00",
|
||||
"shares": "200.00",
|
||||
"symbol": "FGKFX"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"org": {
|
||||
"domain": "www.newfidelity.com",
|
||||
"name": "Investments",
|
||||
"sfin-url": "https://beta-bridge.simplefin.org/simplefin",
|
||||
"url": "https://www.newfidelity.com"
|
||||
},
|
||||
"id": "ACT-5k6l7m8n-9o0p-1q2r-3s4t",
|
||||
"name": "NerdCorp Series B",
|
||||
"currency": "EUR",
|
||||
"balance": "13579.24",
|
||||
"available-balance": "9876.54",
|
||||
"balance-date": 1705413319,
|
||||
"transactions": [],
|
||||
"holdings": [
|
||||
{
|
||||
"id": "HOL-08f775cd-eedf-4ee5-9f53-241c8efa5bf3",
|
||||
"created": 1705413321,
|
||||
"currency": "",
|
||||
"cost_basis": "7500.00",
|
||||
"description": "Mythical FID GROWTH CO K6",
|
||||
"market_value": "9876.54",
|
||||
"purchase_price": "0.00",
|
||||
"shares": "150.00",
|
||||
"symbol": "FGKFX"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"org": {
|
||||
"domain": "www.randombank2.com",
|
||||
"name": "Mythical RandomSavings",
|
||||
"sfin-url": "https://beta-bridge.simplefin.org/simplefin",
|
||||
"url": "https://www.randombank2.com/"
|
||||
},
|
||||
"id": "ACT-6a7b8c9d-0e1f-2g3h-4i5j",
|
||||
"name": "Unicorn Pot",
|
||||
"currency": "USD",
|
||||
"balance": "10000.00",
|
||||
"available-balance": "7500.00",
|
||||
"balance-date": 1705429002,
|
||||
"transactions": [],
|
||||
"holdings": []
|
||||
},
|
||||
{
|
||||
"org": {
|
||||
"domain": "www.randombank2.com",
|
||||
"name": "Mythical RandomSavings",
|
||||
"sfin-url": "https://beta-bridge.simplefin.org/simplefin",
|
||||
"url": "https://www.randombank2.com/"
|
||||
},
|
||||
"id": "ACT-7a8b9c0d-1e2f-3g4h-5i6j",
|
||||
"name": "Castle Mortgage",
|
||||
"currency": "USD",
|
||||
"balance": "7500.00",
|
||||
"available-balance": "5000.00",
|
||||
"balance-date": 1705429002,
|
||||
"transactions": [],
|
||||
"holdings": []
|
||||
}
|
||||
]
|
||||
}
|
425
tests/components/simplefin/snapshots/test_sensor.ambr
Normal file
425
tests/components/simplefin/snapshots/test_sensor.ambr
Normal file
@ -0,0 +1,425 @@
|
||||
# serializer version: 1
|
||||
# name: test_all_entities[sensor.investments_dr_evil_balance-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.investments_dr_evil_balance',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.MONETARY: 'monetary'>,
|
||||
'original_icon': <AccountType.INVESTMENT: 'mdi:chart-areaspline'>,
|
||||
'original_name': 'Balance',
|
||||
'platform': 'simplefin',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'balance',
|
||||
'unique_id': 'account_ACT-4k5l6m7n-8o9p-1q2r-3s4t_balance',
|
||||
'unit_of_measurement': 'USD',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.investments_dr_evil_balance-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by SimpleFIN API',
|
||||
'device_class': 'monetary',
|
||||
'friendly_name': 'Investments Dr Evil Balance',
|
||||
'icon': <AccountType.INVESTMENT: 'mdi:chart-areaspline'>,
|
||||
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||
'unit_of_measurement': 'USD',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.investments_dr_evil_balance',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '1000000.00',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.investments_my_checking_balance-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.investments_my_checking_balance',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.MONETARY: 'monetary'>,
|
||||
'original_icon': <AccountType.CHECKING: 'mdi:checkbook'>,
|
||||
'original_name': 'Balance',
|
||||
'platform': 'simplefin',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'balance',
|
||||
'unique_id': 'account_ACT-1k2l3m4n-5o6p-7q8r-9s0t_balance',
|
||||
'unit_of_measurement': 'USD',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.investments_my_checking_balance-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by SimpleFIN API',
|
||||
'device_class': 'monetary',
|
||||
'friendly_name': 'Investments My Checking Balance',
|
||||
'icon': <AccountType.CHECKING: 'mdi:checkbook'>,
|
||||
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||
'unit_of_measurement': 'USD',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.investments_my_checking_balance',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '12345.67',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.investments_nerdcorp_series_b_balance-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.investments_nerdcorp_series_b_balance',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.MONETARY: 'monetary'>,
|
||||
'original_icon': <AccountType.INVESTMENT: 'mdi:chart-areaspline'>,
|
||||
'original_name': 'Balance',
|
||||
'platform': 'simplefin',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'balance',
|
||||
'unique_id': 'account_ACT-5k6l7m8n-9o0p-1q2r-3s4t_balance',
|
||||
'unit_of_measurement': 'EUR',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.investments_nerdcorp_series_b_balance-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by SimpleFIN API',
|
||||
'device_class': 'monetary',
|
||||
'friendly_name': 'Investments NerdCorp Series B Balance',
|
||||
'icon': <AccountType.INVESTMENT: 'mdi:chart-areaspline'>,
|
||||
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||
'unit_of_measurement': 'EUR',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.investments_nerdcorp_series_b_balance',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '13579.24',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.mythical_randomsavings_castle_mortgage_balance-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.mythical_randomsavings_castle_mortgage_balance',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.MONETARY: 'monetary'>,
|
||||
'original_icon': <AccountType.UNKNOWN: 'mdi:cash'>,
|
||||
'original_name': 'Balance',
|
||||
'platform': 'simplefin',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'balance',
|
||||
'unique_id': 'account_ACT-7a8b9c0d-1e2f-3g4h-5i6j_balance',
|
||||
'unit_of_measurement': 'USD',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.mythical_randomsavings_castle_mortgage_balance-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by SimpleFIN API',
|
||||
'device_class': 'monetary',
|
||||
'friendly_name': 'Mythical RandomSavings Castle Mortgage Balance',
|
||||
'icon': <AccountType.UNKNOWN: 'mdi:cash'>,
|
||||
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||
'unit_of_measurement': 'USD',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.mythical_randomsavings_castle_mortgage_balance',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '7500.00',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.mythical_randomsavings_unicorn_pot_balance-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.mythical_randomsavings_unicorn_pot_balance',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.MONETARY: 'monetary'>,
|
||||
'original_icon': <AccountType.UNKNOWN: 'mdi:cash'>,
|
||||
'original_name': 'Balance',
|
||||
'platform': 'simplefin',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'balance',
|
||||
'unique_id': 'account_ACT-6a7b8c9d-0e1f-2g3h-4i5j_balance',
|
||||
'unit_of_measurement': 'USD',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.mythical_randomsavings_unicorn_pot_balance-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by SimpleFIN API',
|
||||
'device_class': 'monetary',
|
||||
'friendly_name': 'Mythical RandomSavings Unicorn Pot Balance',
|
||||
'icon': <AccountType.UNKNOWN: 'mdi:cash'>,
|
||||
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||
'unit_of_measurement': 'USD',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.mythical_randomsavings_unicorn_pot_balance',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '10000.00',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.random_bank_costco_anywhere_visa_r_card_balance-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.random_bank_costco_anywhere_visa_r_card_balance',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.MONETARY: 'monetary'>,
|
||||
'original_icon': <AccountType.CREDIT_CARD: 'mdi:credit-card'>,
|
||||
'original_name': 'Balance',
|
||||
'platform': 'simplefin',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'balance',
|
||||
'unique_id': 'account_ACT-3a4b5c6d-7e8f-9g0h-1i2j_balance',
|
||||
'unit_of_measurement': 'USD',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.random_bank_costco_anywhere_visa_r_card_balance-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by SimpleFIN API',
|
||||
'device_class': 'monetary',
|
||||
'friendly_name': 'Random Bank Costco Anywhere Visa® Card Balance',
|
||||
'icon': <AccountType.CREDIT_CARD: 'mdi:credit-card'>,
|
||||
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||
'unit_of_measurement': 'USD',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.random_bank_costco_anywhere_visa_r_card_balance',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '-532.69',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.the_bank_of_go_prime_savings_balance-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.the_bank_of_go_prime_savings_balance',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.MONETARY: 'monetary'>,
|
||||
'original_icon': <AccountType.SAVINGS: 'mdi:piggy-bank-outline'>,
|
||||
'original_name': 'Balance',
|
||||
'platform': 'simplefin',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'balance',
|
||||
'unique_id': 'account_ACT-2a3b4c5d-6e7f-8g9h-0i1j_balance',
|
||||
'unit_of_measurement': 'EUR',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.the_bank_of_go_prime_savings_balance-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by SimpleFIN API',
|
||||
'device_class': 'monetary',
|
||||
'friendly_name': 'The Bank of Go PRIME SAVINGS Balance',
|
||||
'icon': <AccountType.SAVINGS: 'mdi:piggy-bank-outline'>,
|
||||
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||
'unit_of_measurement': 'EUR',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.the_bank_of_go_prime_savings_balance',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '9876.54',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.the_bank_of_go_the_bank_balance-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.the_bank_of_go_the_bank_balance',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.MONETARY: 'monetary'>,
|
||||
'original_icon': <AccountType.UNKNOWN: 'mdi:cash'>,
|
||||
'original_name': 'Balance',
|
||||
'platform': 'simplefin',
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'balance',
|
||||
'unique_id': 'account_ACT-1a2b3c4d-5e6f-7g8h-9i0j_balance',
|
||||
'unit_of_measurement': 'USD',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[sensor.the_bank_of_go_the_bank_balance-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'attribution': 'Data provided by SimpleFIN API',
|
||||
'device_class': 'monetary',
|
||||
'friendly_name': 'The Bank of Go The Bank Balance',
|
||||
'icon': <AccountType.UNKNOWN: 'mdi:cash'>,
|
||||
'state_class': <SensorStateClass.TOTAL: 'total'>,
|
||||
'unit_of_measurement': 'USD',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.the_bank_of_go_the_bank_balance',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '7777.77',
|
||||
})
|
||||
# ---
|
164
tests/components/simplefin/test_config_flow.py
Normal file
164
tests/components/simplefin/test_config_flow.py
Normal file
@ -0,0 +1,164 @@
|
||||
"""Test config flow."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
from simplefin4py.exceptions import (
|
||||
SimpleFinAuthError,
|
||||
SimpleFinClaimError,
|
||||
SimpleFinInvalidAccountURLError,
|
||||
SimpleFinInvalidClaimTokenError,
|
||||
SimpleFinPaymentRequiredError,
|
||||
)
|
||||
|
||||
from homeassistant.components.simplefin import CONF_ACCESS_URL
|
||||
from homeassistant.components.simplefin.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .conftest import MOCK_ACCESS_URL
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_successful_claim(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_simplefin_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test successful token claim in config flow."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert not result["errors"]
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_ACCESS_URL: "donJulio"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "SimpleFIN"
|
||||
assert result["data"] == {CONF_ACCESS_URL: MOCK_ACCESS_URL}
|
||||
|
||||
|
||||
async def test_already_setup(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_simplefin_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test all entities."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_ACCESS_URL: MOCK_ACCESS_URL},
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_access_url(
|
||||
hass: HomeAssistant,
|
||||
mock_simplefin_client: AsyncMock,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test standard config flow."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_ACCESS_URL: "http://user:password@string"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["data"][CONF_ACCESS_URL] == "http://user:password@string"
|
||||
assert result["title"] == "SimpleFIN"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect", "error_key"),
|
||||
[
|
||||
(SimpleFinInvalidAccountURLError, "url_error"),
|
||||
(SimpleFinPaymentRequiredError, "payment_required"),
|
||||
(SimpleFinAuthError, "invalid_auth"),
|
||||
],
|
||||
)
|
||||
async def test_access_url_errors(
|
||||
hass: HomeAssistant,
|
||||
mock_simplefin_client: AsyncMock,
|
||||
side_effect: Exception,
|
||||
error_key: str,
|
||||
) -> None:
|
||||
"""Test the various errors we can get in access_url mode."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
mock_simplefin_client.claim_setup_token.side_effect = side_effect
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_ACCESS_URL: "donJulio"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": error_key}
|
||||
|
||||
mock_simplefin_client.claim_setup_token.side_effect = None
|
||||
|
||||
# Pass the entry creation
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_ACCESS_URL: "http://user:password@string"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {CONF_ACCESS_URL: "http://user:password@string"}
|
||||
assert result["title"] == "SimpleFIN"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect", "error_key"),
|
||||
[
|
||||
(SimpleFinInvalidClaimTokenError, "invalid_claim_token"),
|
||||
(SimpleFinClaimError, "claim_error"),
|
||||
],
|
||||
)
|
||||
async def test_claim_token_errors(
|
||||
hass: HomeAssistant,
|
||||
mock_simplefin_client: AsyncMock,
|
||||
side_effect: Exception,
|
||||
error_key: str,
|
||||
) -> None:
|
||||
"""Test config flow with various token claim errors."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
mock_simplefin_client.claim_setup_token.side_effect = side_effect
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_ACCESS_URL: "donJulio"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": error_key}
|
||||
|
||||
mock_simplefin_client.claim_setup_token.side_effect = None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_ACCESS_URL: "donJulio"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {CONF_ACCESS_URL: "https://i:am@yomama.house.com"}
|
||||
assert result["title"] == "SimpleFIN"
|
94
tests/components/simplefin/test_sensor.py
Normal file
94
tests/components/simplefin/test_sensor.py
Normal file
@ -0,0 +1,94 @@
|
||||
"""Test SimpleFin Sensor with Snapshot data."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from simplefin4py.exceptions import SimpleFinAuthError, SimpleFinPaymentRequiredError
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
from homeassistant.const import STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
|
||||
|
||||
async def test_all_entities(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_simplefin_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test all entities."""
|
||||
with patch("homeassistant.components.simplefin.PLATFORMS", [Platform.SENSOR]):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect"),
|
||||
[
|
||||
(SimpleFinAuthError),
|
||||
(SimpleFinPaymentRequiredError),
|
||||
],
|
||||
)
|
||||
async def test_update_errors(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_simplefin_client: AsyncMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
side_effect: Exception,
|
||||
) -> None:
|
||||
"""Test connection error."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert hass.states.get("sensor.the_bank_of_go_the_bank_balance").state == "7777.77"
|
||||
assert hass.states.get("sensor.investments_my_checking_balance").state == "12345.67"
|
||||
assert (
|
||||
hass.states.get("sensor.the_bank_of_go_prime_savings_balance").state
|
||||
== "9876.54"
|
||||
)
|
||||
assert (
|
||||
hass.states.get("sensor.random_bank_costco_anywhere_visa_r_card_balance").state
|
||||
== "-532.69"
|
||||
)
|
||||
assert hass.states.get("sensor.investments_dr_evil_balance").state == "1000000.00"
|
||||
assert (
|
||||
hass.states.get("sensor.investments_nerdcorp_series_b_balance").state
|
||||
== "13579.24"
|
||||
)
|
||||
assert (
|
||||
hass.states.get("sensor.mythical_randomsavings_unicorn_pot_balance").state
|
||||
== "10000.00"
|
||||
)
|
||||
assert (
|
||||
hass.states.get("sensor.mythical_randomsavings_castle_mortgage_balance").state
|
||||
== "7500.00"
|
||||
)
|
||||
|
||||
mock_simplefin_client.return_value.fetch_data.side_effect = side_effect
|
||||
freezer.tick(timedelta(days=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
sensors = [
|
||||
"sensor.the_bank_of_go_the_bank_balance",
|
||||
"sensor.investments_my_checking_balance",
|
||||
"sensor.the_bank_of_go_prime_savings_balance",
|
||||
"sensor.random_bank_costco_anywhere_visa_r_card_balance",
|
||||
"sensor.investments_dr_evil_balance",
|
||||
"sensor.investments_nerdcorp_series_b_balance",
|
||||
"sensor.mythical_randomsavings_unicorn_pot_balance",
|
||||
"sensor.mythical_randomsavings_castle_mortgage_balance",
|
||||
]
|
||||
|
||||
for sensor in sensors:
|
||||
assert hass.states.get(sensor).state == STATE_UNAVAILABLE
|
Loading…
x
Reference in New Issue
Block a user