-
-
Notifications
You must be signed in to change notification settings - Fork 32.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Initial monzo implementation * Tests and fixes * Extracted api to pypi package * Add app confirmation step * Corrected data path for accounts * Removed useless check * Improved tests * Exclude partially tested files from coverage check * Use has_entity_name naming * Bumped monzopy to 1.0.10 * Remove commented out code * Remove reauth from initial PR * Remove useless code * Correct comment * Remove reauth tests * Remove device triggers from intial PR * Set attr outside constructor * Remove f-strings where no longer needed in entity.py * Rename field to make clearer it's a Callable * Correct native_unit_of_measurement * Remove pot transfer service from intial PR * Remove reauth string * Remove empty fields in manifest.json * Freeze SensorEntityDescription and remove Mixin Also use list comprehensions for producing sensor lists * Use consts in application_credentials.py * Revert "Remove useless code" Apparently this wasn't useless This reverts commit c6b7109. * Ruff and pylint style fixes * Bumped monzopy to 1.1.0 Adds support for joint/business/etc account pots * Update test snapshot * Rename AsyncConfigEntryAuth * Use dataclasses instead of dictionaries * Move OAuth constants to application_credentials.py * Remove remaining constants and dependencies for services from this PR * Remove empty manifest entry * Fix comment * Set device entry_type to service * ACC_SENSORS -> ACCOUNT_SENSORS * Make value_fn of sensors return StateType * Rename OAuthMonzoAPI again * Fix tests * Patch API instead of integration for unavailable test * Move pot constant to sensor.py * Improve type safety in async_get_monzo_api_data() * Update async_oauth_create_entry() docstring --------- Co-authored-by: Erik Montnemery <erik@montnemery.com>
- Loading branch information
1 parent
5bef2d5
commit 6e024d5
Showing
24 changed files
with
919 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
"""The Monzo integration.""" | ||
|
||
from __future__ import annotations | ||
|
||
from datetime import timedelta | ||
import logging | ||
|
||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.const import Platform | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow | ||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator | ||
|
||
from .api import AuthenticatedMonzoAPI | ||
from .const import DOMAIN | ||
from .data import MonzoData, MonzoSensorData | ||
|
||
PLATFORMS: list[Platform] = [Platform.SENSOR] | ||
|
||
|
||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: | ||
"""Set up Monzo from a config entry.""" | ||
implementation = ( | ||
await config_entry_oauth2_flow.async_get_config_entry_implementation( | ||
hass, entry | ||
) | ||
) | ||
|
||
async def async_get_monzo_api_data() -> MonzoSensorData: | ||
monzo_data: MonzoData = hass.data[DOMAIN][entry.entry_id] | ||
accounts = await external_api.user_account.accounts() | ||
pots = await external_api.user_account.pots() | ||
monzo_data.accounts = accounts | ||
monzo_data.pots = pots | ||
return MonzoSensorData(accounts=accounts, pots=pots) | ||
|
||
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation) | ||
|
||
external_api = AuthenticatedMonzoAPI( | ||
aiohttp_client.async_get_clientsession(hass), session | ||
) | ||
|
||
coordinator = DataUpdateCoordinator( | ||
hass, | ||
logging.getLogger(__name__), | ||
name=DOMAIN, | ||
update_method=async_get_monzo_api_data, | ||
update_interval=timedelta(minutes=1), | ||
) | ||
hass.data.setdefault(DOMAIN, {}) | ||
hass.data[DOMAIN][entry.entry_id] = MonzoData(external_api, coordinator) | ||
|
||
await coordinator.async_config_entry_first_refresh() | ||
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.""" | ||
data = hass.data[DOMAIN] | ||
|
||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) | ||
|
||
if unload_ok and entry.entry_id in data: | ||
data.pop(entry.entry_id) | ||
|
||
return unload_ok |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
"""API for Monzo bound to Home Assistant OAuth.""" | ||
|
||
from aiohttp import ClientSession | ||
from monzopy import AbstractMonzoApi | ||
|
||
from homeassistant.helpers import config_entry_oauth2_flow | ||
|
||
|
||
class AuthenticatedMonzoAPI(AbstractMonzoApi): | ||
"""A Monzo API instance with authentication tied to an OAuth2 based config entry.""" | ||
|
||
def __init__( | ||
self, | ||
websession: ClientSession, | ||
oauth_session: config_entry_oauth2_flow.OAuth2Session, | ||
) -> None: | ||
"""Initialize Monzo auth.""" | ||
super().__init__(websession) | ||
self._oauth_session = oauth_session | ||
|
||
async def async_get_access_token(self) -> str: | ||
"""Return a valid access token.""" | ||
if not self._oauth_session.valid_token: | ||
await self._oauth_session.async_ensure_token_valid() | ||
|
||
return str(self._oauth_session.token["access_token"]) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
"""application_credentials platform the Monzo integration.""" | ||
|
||
from homeassistant.components.application_credentials import AuthorizationServer | ||
from homeassistant.core import HomeAssistant | ||
|
||
OAUTH2_AUTHORIZE = "https://auth.monzo.com" | ||
OAUTH2_TOKEN = "https://api.monzo.com/oauth2/token" | ||
|
||
|
||
async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer: | ||
"""Return authorization server.""" | ||
return AuthorizationServer( | ||
authorize_url=OAUTH2_AUTHORIZE, | ||
token_url=OAUTH2_TOKEN, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
"""Config flow for Monzo.""" | ||
|
||
from __future__ import annotations | ||
|
||
import logging | ||
from typing import Any | ||
|
||
import voluptuous as vol | ||
|
||
from homeassistant.config_entries import ConfigFlowResult | ||
from homeassistant.const import CONF_TOKEN | ||
from homeassistant.helpers import config_entry_oauth2_flow | ||
|
||
from .const import DOMAIN | ||
|
||
|
||
class MonzoFlowHandler( | ||
config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN | ||
): | ||
"""Handle a config flow.""" | ||
|
||
DOMAIN = DOMAIN | ||
|
||
oauth_data: dict[str, Any] | ||
|
||
@property | ||
def logger(self) -> logging.Logger: | ||
"""Return logger.""" | ||
return logging.getLogger(__name__) | ||
|
||
async def async_step_await_approval_confirmation( | ||
self, user_input: dict[str, Any] | None = None | ||
) -> ConfigFlowResult: | ||
"""Wait for the user to confirm in-app approval.""" | ||
if user_input is not None: | ||
return self.async_create_entry(title=DOMAIN, data={**self.oauth_data}) | ||
|
||
data_schema = vol.Schema({vol.Required("confirm"): bool}) | ||
|
||
return self.async_show_form( | ||
step_id="await_approval_confirmation", data_schema=data_schema | ||
) | ||
|
||
async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult: | ||
"""Create an entry for the flow.""" | ||
user_id = str(data[CONF_TOKEN]["user_id"]) | ||
await self.async_set_unique_id(user_id) | ||
self._abort_if_unique_id_configured() | ||
|
||
self.oauth_data = data | ||
|
||
return await self.async_step_await_approval_confirmation() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
"""Constants for the Monzo integration.""" | ||
|
||
DOMAIN = "monzo" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
"""Dataclass for Monzo data.""" | ||
|
||
from dataclasses import dataclass, field | ||
from typing import Any | ||
|
||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator | ||
|
||
from .api import AuthenticatedMonzoAPI | ||
|
||
|
||
@dataclass(kw_only=True) | ||
class MonzoSensorData: | ||
"""A dataclass for holding sensor data returned by the DataUpdateCoordinator.""" | ||
|
||
accounts: list[dict[str, Any]] = field(default_factory=list) | ||
pots: list[dict[str, Any]] = field(default_factory=list) | ||
|
||
|
||
@dataclass | ||
class MonzoData(MonzoSensorData): | ||
"""A dataclass for holding data stored in hass.data.""" | ||
|
||
external_api: AuthenticatedMonzoAPI | ||
coordinator: DataUpdateCoordinator[MonzoSensorData] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
"""Base entity for Monzo.""" | ||
|
||
from __future__ import annotations | ||
|
||
from collections.abc import Callable | ||
from typing import Any | ||
|
||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo | ||
from homeassistant.helpers.update_coordinator import ( | ||
CoordinatorEntity, | ||
DataUpdateCoordinator, | ||
) | ||
|
||
from .const import DOMAIN | ||
from .data import MonzoSensorData | ||
|
||
|
||
class MonzoBaseEntity(CoordinatorEntity[DataUpdateCoordinator[MonzoSensorData]]): | ||
"""Common base for Monzo entities.""" | ||
|
||
_attr_attribution = "Data provided by Monzo" | ||
_attr_has_entity_name = True | ||
|
||
def __init__( | ||
self, | ||
coordinator: DataUpdateCoordinator[MonzoSensorData], | ||
index: int, | ||
device_model: str, | ||
data_accessor: Callable[[MonzoSensorData], list[dict[str, Any]]], | ||
) -> None: | ||
"""Initialize sensor.""" | ||
super().__init__(coordinator) | ||
self.index = index | ||
self._data_accessor = data_accessor | ||
|
||
self._attr_device_info = DeviceInfo( | ||
entry_type=DeviceEntryType.SERVICE, | ||
identifiers={(DOMAIN, str(self.data["id"]))}, | ||
manufacturer="Monzo", | ||
model=device_model, | ||
name=self.data["name"], | ||
) | ||
|
||
@property | ||
def data(self) -> dict[str, Any]: | ||
"""Shortcut to access coordinator data for the entity.""" | ||
return self._data_accessor(self.coordinator.data)[self.index] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
{ | ||
"domain": "monzo", | ||
"name": "Monzo", | ||
"codeowners": ["@jakemartin-icl"], | ||
"config_flow": true, | ||
"dependencies": ["application_credentials"], | ||
"documentation": "https://www.home-assistant.io/integrations/monzo", | ||
"iot_class": "cloud_polling", | ||
"requirements": ["monzopy==1.1.0"] | ||
} |
Oops, something went wrong.