mirror of
https://github.com/home-assistant/core.git
synced 2025-07-17 02:07:09 +00:00
Add Monzo integration (#101731)
* 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 c6b7109e47202f866c766ea4c16ce3eb0588795b. * 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>
This commit is contained in:
parent
5bef2d5d25
commit
6e024d54f1
@ -810,6 +810,8 @@ omit =
|
|||||||
homeassistant/components/moehlenhoff_alpha2/binary_sensor.py
|
homeassistant/components/moehlenhoff_alpha2/binary_sensor.py
|
||||||
homeassistant/components/moehlenhoff_alpha2/climate.py
|
homeassistant/components/moehlenhoff_alpha2/climate.py
|
||||||
homeassistant/components/moehlenhoff_alpha2/sensor.py
|
homeassistant/components/moehlenhoff_alpha2/sensor.py
|
||||||
|
homeassistant/components/monzo/__init__.py
|
||||||
|
homeassistant/components/monzo/api.py
|
||||||
homeassistant/components/motion_blinds/__init__.py
|
homeassistant/components/motion_blinds/__init__.py
|
||||||
homeassistant/components/motion_blinds/coordinator.py
|
homeassistant/components/motion_blinds/coordinator.py
|
||||||
homeassistant/components/motion_blinds/cover.py
|
homeassistant/components/motion_blinds/cover.py
|
||||||
|
@ -300,6 +300,7 @@ homeassistant.components.minecraft_server.*
|
|||||||
homeassistant.components.mjpeg.*
|
homeassistant.components.mjpeg.*
|
||||||
homeassistant.components.modbus.*
|
homeassistant.components.modbus.*
|
||||||
homeassistant.components.modem_callerid.*
|
homeassistant.components.modem_callerid.*
|
||||||
|
homeassistant.components.monzo.*
|
||||||
homeassistant.components.moon.*
|
homeassistant.components.moon.*
|
||||||
homeassistant.components.mopeka.*
|
homeassistant.components.mopeka.*
|
||||||
homeassistant.components.motionmount.*
|
homeassistant.components.motionmount.*
|
||||||
|
@ -867,6 +867,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/moehlenhoff_alpha2/ @j-a-n
|
/tests/components/moehlenhoff_alpha2/ @j-a-n
|
||||||
/homeassistant/components/monoprice/ @etsinko @OnFreund
|
/homeassistant/components/monoprice/ @etsinko @OnFreund
|
||||||
/tests/components/monoprice/ @etsinko @OnFreund
|
/tests/components/monoprice/ @etsinko @OnFreund
|
||||||
|
/homeassistant/components/monzo/ @jakemartin-icl
|
||||||
|
/tests/components/monzo/ @jakemartin-icl
|
||||||
/homeassistant/components/moon/ @fabaff @frenck
|
/homeassistant/components/moon/ @fabaff @frenck
|
||||||
/tests/components/moon/ @fabaff @frenck
|
/tests/components/moon/ @fabaff @frenck
|
||||||
/homeassistant/components/mopeka/ @bdraco
|
/homeassistant/components/mopeka/ @bdraco
|
||||||
|
68
homeassistant/components/monzo/__init__.py
Normal file
68
homeassistant/components/monzo/__init__.py
Normal file
@ -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
|
26
homeassistant/components/monzo/api.py
Normal file
26
homeassistant/components/monzo/api.py
Normal file
@ -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"])
|
15
homeassistant/components/monzo/application_credentials.py
Normal file
15
homeassistant/components/monzo/application_credentials.py
Normal file
@ -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,
|
||||||
|
)
|
52
homeassistant/components/monzo/config_flow.py
Normal file
52
homeassistant/components/monzo/config_flow.py
Normal file
@ -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()
|
3
homeassistant/components/monzo/const.py
Normal file
3
homeassistant/components/monzo/const.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"""Constants for the Monzo integration."""
|
||||||
|
|
||||||
|
DOMAIN = "monzo"
|
24
homeassistant/components/monzo/data.py
Normal file
24
homeassistant/components/monzo/data.py
Normal file
@ -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]
|
47
homeassistant/components/monzo/entity.py
Normal file
47
homeassistant/components/monzo/entity.py
Normal file
@ -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]
|
10
homeassistant/components/monzo/manifest.json
Normal file
10
homeassistant/components/monzo/manifest.json
Normal file
@ -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"]
|
||||||
|
}
|
123
homeassistant/components/monzo/sensor.py
Normal file
123
homeassistant/components/monzo/sensor.py
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
"""Platform for sensor integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import (
|
||||||
|
SensorDeviceClass,
|
||||||
|
SensorEntity,
|
||||||
|
SensorEntityDescription,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.helpers.typing import StateType
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .data import MonzoSensorData
|
||||||
|
from .entity import MonzoBaseEntity
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class MonzoSensorEntityDescription(SensorEntityDescription):
|
||||||
|
"""Describes Monzo sensor entity."""
|
||||||
|
|
||||||
|
value_fn: Callable[[dict[str, Any]], StateType]
|
||||||
|
|
||||||
|
|
||||||
|
ACCOUNT_SENSORS = (
|
||||||
|
MonzoSensorEntityDescription(
|
||||||
|
key="balance",
|
||||||
|
translation_key="balance",
|
||||||
|
value_fn=lambda data: data["balance"]["balance"] / 100,
|
||||||
|
device_class=SensorDeviceClass.MONETARY,
|
||||||
|
native_unit_of_measurement="GBP",
|
||||||
|
suggested_display_precision=2,
|
||||||
|
),
|
||||||
|
MonzoSensorEntityDescription(
|
||||||
|
key="total_balance",
|
||||||
|
translation_key="total_balance",
|
||||||
|
value_fn=lambda data: data["balance"]["total_balance"] / 100,
|
||||||
|
device_class=SensorDeviceClass.MONETARY,
|
||||||
|
native_unit_of_measurement="GBP",
|
||||||
|
suggested_display_precision=2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
POT_SENSORS = (
|
||||||
|
MonzoSensorEntityDescription(
|
||||||
|
key="pot_balance",
|
||||||
|
translation_key="pot_balance",
|
||||||
|
value_fn=lambda data: data["balance"] / 100,
|
||||||
|
device_class=SensorDeviceClass.MONETARY,
|
||||||
|
native_unit_of_measurement="GBP",
|
||||||
|
suggested_display_precision=2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
MODEL_POT = "Pot"
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Defer sensor setup to the shared sensor module."""
|
||||||
|
coordinator = hass.data[DOMAIN][config_entry.entry_id].coordinator
|
||||||
|
|
||||||
|
accounts = [
|
||||||
|
MonzoSensor(
|
||||||
|
coordinator,
|
||||||
|
entity_description,
|
||||||
|
index,
|
||||||
|
account["name"],
|
||||||
|
lambda x: x.accounts,
|
||||||
|
)
|
||||||
|
for entity_description in ACCOUNT_SENSORS
|
||||||
|
for index, account in enumerate(
|
||||||
|
hass.data[DOMAIN][config_entry.entry_id].accounts
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
pots = [
|
||||||
|
MonzoSensor(coordinator, entity_description, index, MODEL_POT, lambda x: x.pots)
|
||||||
|
for entity_description in POT_SENSORS
|
||||||
|
for index, _pot in enumerate(hass.data[DOMAIN][config_entry.entry_id].pots)
|
||||||
|
]
|
||||||
|
|
||||||
|
async_add_entities(accounts + pots)
|
||||||
|
|
||||||
|
|
||||||
|
class MonzoSensor(MonzoBaseEntity, SensorEntity):
|
||||||
|
"""Represents a Monzo sensor."""
|
||||||
|
|
||||||
|
entity_description: MonzoSensorEntityDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: DataUpdateCoordinator[MonzoSensorData],
|
||||||
|
entity_description: MonzoSensorEntityDescription,
|
||||||
|
index: int,
|
||||||
|
device_model: str,
|
||||||
|
data_accessor: Callable[[MonzoSensorData], list[dict[str, Any]]],
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
super().__init__(coordinator, index, device_model, data_accessor)
|
||||||
|
self.entity_description = entity_description
|
||||||
|
self._attr_unique_id = f"{self.data['id']}_{entity_description.key}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> StateType:
|
||||||
|
"""Return the state."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
state = self.entity_description.value_fn(self.data)
|
||||||
|
except (KeyError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
return state
|
41
homeassistant/components/monzo/strings.json
Normal file
41
homeassistant/components/monzo/strings.json
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"pick_implementation": {
|
||||||
|
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
|
||||||
|
},
|
||||||
|
"await_approval_confirmation": {
|
||||||
|
"title": "Confirm in Monzo app",
|
||||||
|
"description": "Before proceeding, open your Monzo app and approve the request from Home Assistant.",
|
||||||
|
"data": {
|
||||||
|
"confirm": "I've approved"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||||
|
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||||
|
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
|
||||||
|
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
|
||||||
|
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
|
||||||
|
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
|
||||||
|
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]"
|
||||||
|
},
|
||||||
|
"create_entry": {
|
||||||
|
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"balance": {
|
||||||
|
"name": "Balance"
|
||||||
|
},
|
||||||
|
"total_balance": {
|
||||||
|
"name": "Total Balance"
|
||||||
|
},
|
||||||
|
"pot_balance": {
|
||||||
|
"name": "Balance"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,7 @@ APPLICATION_CREDENTIALS = [
|
|||||||
"lametric",
|
"lametric",
|
||||||
"lyric",
|
"lyric",
|
||||||
"microbees",
|
"microbees",
|
||||||
|
"monzo",
|
||||||
"myuplink",
|
"myuplink",
|
||||||
"neato",
|
"neato",
|
||||||
"nest",
|
"nest",
|
||||||
|
@ -331,6 +331,7 @@ FLOWS = {
|
|||||||
"modern_forms",
|
"modern_forms",
|
||||||
"moehlenhoff_alpha2",
|
"moehlenhoff_alpha2",
|
||||||
"monoprice",
|
"monoprice",
|
||||||
|
"monzo",
|
||||||
"moon",
|
"moon",
|
||||||
"mopeka",
|
"mopeka",
|
||||||
"motion_blinds",
|
"motion_blinds",
|
||||||
|
@ -3739,6 +3739,12 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"iot_class": "local_polling"
|
"iot_class": "local_polling"
|
||||||
},
|
},
|
||||||
|
"monzo": {
|
||||||
|
"name": "Monzo",
|
||||||
|
"integration_type": "hub",
|
||||||
|
"config_flow": true,
|
||||||
|
"iot_class": "cloud_polling"
|
||||||
|
},
|
||||||
"moon": {
|
"moon": {
|
||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
|
10
mypy.ini
10
mypy.ini
@ -2762,6 +2762,16 @@ disallow_untyped_defs = true
|
|||||||
warn_return_any = true
|
warn_return_any = true
|
||||||
warn_unreachable = true
|
warn_unreachable = true
|
||||||
|
|
||||||
|
[mypy-homeassistant.components.monzo.*]
|
||||||
|
check_untyped_defs = true
|
||||||
|
disallow_incomplete_defs = true
|
||||||
|
disallow_subclassing_any = true
|
||||||
|
disallow_untyped_calls = true
|
||||||
|
disallow_untyped_decorators = true
|
||||||
|
disallow_untyped_defs = true
|
||||||
|
warn_return_any = true
|
||||||
|
warn_unreachable = true
|
||||||
|
|
||||||
[mypy-homeassistant.components.moon.*]
|
[mypy-homeassistant.components.moon.*]
|
||||||
check_untyped_defs = true
|
check_untyped_defs = true
|
||||||
disallow_incomplete_defs = true
|
disallow_incomplete_defs = true
|
||||||
|
@ -1325,6 +1325,9 @@ moat-ble==0.1.1
|
|||||||
# homeassistant.components.moehlenhoff_alpha2
|
# homeassistant.components.moehlenhoff_alpha2
|
||||||
moehlenhoff-alpha2==1.3.0
|
moehlenhoff-alpha2==1.3.0
|
||||||
|
|
||||||
|
# homeassistant.components.monzo
|
||||||
|
monzopy==1.1.0
|
||||||
|
|
||||||
# homeassistant.components.mopeka
|
# homeassistant.components.mopeka
|
||||||
mopeka-iot-ble==0.7.0
|
mopeka-iot-ble==0.7.0
|
||||||
|
|
||||||
|
@ -1067,6 +1067,9 @@ moat-ble==0.1.1
|
|||||||
# homeassistant.components.moehlenhoff_alpha2
|
# homeassistant.components.moehlenhoff_alpha2
|
||||||
moehlenhoff-alpha2==1.3.0
|
moehlenhoff-alpha2==1.3.0
|
||||||
|
|
||||||
|
# homeassistant.components.monzo
|
||||||
|
monzopy==1.1.0
|
||||||
|
|
||||||
# homeassistant.components.mopeka
|
# homeassistant.components.mopeka
|
||||||
mopeka-iot-ble==0.7.0
|
mopeka-iot-ble==0.7.0
|
||||||
|
|
||||||
|
12
tests/components/monzo/__init__.py
Normal file
12
tests/components/monzo/__init__.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
"""Tests for the Monzo integration."""
|
||||||
|
|
||||||
|
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)
|
125
tests/components/monzo/conftest.py
Normal file
125
tests/components/monzo/conftest.py
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
"""Fixtures for tests."""
|
||||||
|
|
||||||
|
import time
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
from monzopy.monzopy import UserAccount
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.application_credentials import (
|
||||||
|
ClientCredential,
|
||||||
|
async_import_client_credential,
|
||||||
|
)
|
||||||
|
from homeassistant.components.monzo.api import AuthenticatedMonzoAPI
|
||||||
|
from homeassistant.components.monzo.const import DOMAIN
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
CLIENT_ID = "1234"
|
||||||
|
CLIENT_SECRET = "5678"
|
||||||
|
TEST_ACCOUNTS = [
|
||||||
|
{
|
||||||
|
"id": "acc_curr",
|
||||||
|
"name": "Current Account",
|
||||||
|
"type": "uk_retail",
|
||||||
|
"balance": {"balance": 123, "total_balance": 321},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "acc_flex",
|
||||||
|
"name": "Flex",
|
||||||
|
"type": "uk_monzo_flex",
|
||||||
|
"balance": {"balance": 123, "total_balance": 321},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
TEST_POTS = [
|
||||||
|
{
|
||||||
|
"id": "pot_savings",
|
||||||
|
"name": "Savings",
|
||||||
|
"style": "savings",
|
||||||
|
"balance": 134578,
|
||||||
|
"currency": "GBP",
|
||||||
|
"type": "instant_access",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
TITLE = "jake"
|
||||||
|
USER_ID = 12345
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
async def setup_credentials(hass: HomeAssistant) -> None:
|
||||||
|
"""Fixture to setup credentials."""
|
||||||
|
assert await async_setup_component(hass, "application_credentials", {})
|
||||||
|
await async_import_client_credential(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
ClientCredential(CLIENT_ID, CLIENT_SECRET, DOMAIN),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="expires_at")
|
||||||
|
def mock_expires_at() -> int:
|
||||||
|
"""Fixture to set the oauth token expiration time."""
|
||||||
|
return time.time() + 3600
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def polling_config_entry(expires_at: int) -> MockConfigEntry:
|
||||||
|
"""Create Monzo entry in Home Assistant."""
|
||||||
|
return MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title=TITLE,
|
||||||
|
unique_id=str(USER_ID),
|
||||||
|
data={
|
||||||
|
"auth_implementation": DOMAIN,
|
||||||
|
"token": {
|
||||||
|
"status": 0,
|
||||||
|
"userid": str(USER_ID),
|
||||||
|
"access_token": "mock-access-token",
|
||||||
|
"refresh_token": "mock-refresh-token",
|
||||||
|
"expires_in": 60,
|
||||||
|
"expires_at": time.time() + 1000,
|
||||||
|
},
|
||||||
|
"profile": TITLE,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="basic_monzo")
|
||||||
|
def mock_basic_monzo():
|
||||||
|
"""Mock monzo with one pot."""
|
||||||
|
|
||||||
|
mock = AsyncMock(spec=AuthenticatedMonzoAPI)
|
||||||
|
mock_user_account = AsyncMock(spec=UserAccount)
|
||||||
|
|
||||||
|
mock_user_account.accounts.return_value = []
|
||||||
|
|
||||||
|
mock_user_account.pots.return_value = TEST_POTS
|
||||||
|
|
||||||
|
mock.user_account = mock_user_account
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.monzo.AuthenticatedMonzoAPI",
|
||||||
|
return_value=mock,
|
||||||
|
):
|
||||||
|
yield mock
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="monzo")
|
||||||
|
def mock_monzo():
|
||||||
|
"""Mock monzo."""
|
||||||
|
|
||||||
|
mock = AsyncMock(spec=AuthenticatedMonzoAPI)
|
||||||
|
mock_user_account = AsyncMock(spec=UserAccount)
|
||||||
|
|
||||||
|
mock_user_account.accounts.return_value = TEST_ACCOUNTS
|
||||||
|
mock_user_account.pots.return_value = TEST_POTS
|
||||||
|
|
||||||
|
mock.user_account = mock_user_account
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.monzo.AuthenticatedMonzoAPI",
|
||||||
|
return_value=mock,
|
||||||
|
):
|
||||||
|
yield mock
|
65
tests/components/monzo/snapshots/test_sensor.ambr
Normal file
65
tests/components/monzo/snapshots/test_sensor.ambr
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
# serializer version: 1
|
||||||
|
# name: test_all_entities
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'attribution': 'Data provided by Monzo',
|
||||||
|
'device_class': 'monetary',
|
||||||
|
'friendly_name': 'Current Account Balance',
|
||||||
|
'unit_of_measurement': 'GBP',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.current_account_balance',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '1.23',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_entities.1
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'attribution': 'Data provided by Monzo',
|
||||||
|
'device_class': 'monetary',
|
||||||
|
'friendly_name': 'Current Account Total Balance',
|
||||||
|
'unit_of_measurement': 'GBP',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.current_account_total_balance',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '3.21',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_entities.2
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'attribution': 'Data provided by Monzo',
|
||||||
|
'device_class': 'monetary',
|
||||||
|
'friendly_name': 'Flex Balance',
|
||||||
|
'unit_of_measurement': 'GBP',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.flex_balance',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '1.23',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_all_entities.3
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'attribution': 'Data provided by Monzo',
|
||||||
|
'device_class': 'monetary',
|
||||||
|
'friendly_name': 'Flex Total Balance',
|
||||||
|
'unit_of_measurement': 'GBP',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.flex_total_balance',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '3.21',
|
||||||
|
})
|
||||||
|
# ---
|
138
tests/components/monzo/test_config_flow.py
Normal file
138
tests/components/monzo/test_config_flow.py
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
"""Tests for config flow."""
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
from homeassistant.components.monzo.application_credentials import (
|
||||||
|
OAUTH2_AUTHORIZE,
|
||||||
|
OAUTH2_TOKEN,
|
||||||
|
)
|
||||||
|
from homeassistant.components.monzo.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 config_entry_oauth2_flow
|
||||||
|
|
||||||
|
from . import setup_integration
|
||||||
|
from .conftest import CLIENT_ID, USER_ID
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||||
|
from tests.typing import ClientSessionGenerator
|
||||||
|
|
||||||
|
|
||||||
|
async def test_full_flow(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_client_no_auth: ClientSessionGenerator,
|
||||||
|
current_request_with_host: None,
|
||||||
|
aioclient_mock: AiohttpClientMocker,
|
||||||
|
) -> None:
|
||||||
|
"""Check full flow."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
state = config_entry_oauth2_flow._encode_jwt(
|
||||||
|
hass,
|
||||||
|
{
|
||||||
|
"flow_id": result["flow_id"],
|
||||||
|
"redirect_uri": "https://example.com/auth/external/callback",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == FlowResultType.EXTERNAL_STEP
|
||||||
|
assert result["url"] == (
|
||||||
|
f"{OAUTH2_AUTHORIZE}/?"
|
||||||
|
f"response_type=code&client_id={CLIENT_ID}&"
|
||||||
|
"redirect_uri=https://example.com/auth/external/callback&"
|
||||||
|
f"state={state}"
|
||||||
|
)
|
||||||
|
|
||||||
|
client = await hass_client_no_auth()
|
||||||
|
resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
|
||||||
|
assert resp.status == 200
|
||||||
|
assert resp.headers["content-type"] == "text/html; charset=utf-8"
|
||||||
|
|
||||||
|
aioclient_mock.clear_requests()
|
||||||
|
aioclient_mock.post(
|
||||||
|
OAUTH2_TOKEN,
|
||||||
|
json={
|
||||||
|
"refresh_token": "mock-refresh-token",
|
||||||
|
"access_token": "mock-access-token",
|
||||||
|
"type": "Bearer",
|
||||||
|
"expires_in": 60,
|
||||||
|
"user_id": 600,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.monzo.async_setup_entry", return_value=True
|
||||||
|
) as mock_setup:
|
||||||
|
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||||
|
|
||||||
|
assert len(mock_setup.mock_calls) == 0
|
||||||
|
|
||||||
|
assert result["type"] == FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "await_approval_confirmation"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], user_input={"confirm": True}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||||
|
assert len(mock_setup.mock_calls) == 1
|
||||||
|
|
||||||
|
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == DOMAIN
|
||||||
|
assert "result" in result
|
||||||
|
assert result["result"].unique_id == "600"
|
||||||
|
assert "token" in result["result"].data
|
||||||
|
assert result["result"].data["token"]["access_token"] == "mock-access-token"
|
||||||
|
assert result["result"].data["token"]["refresh_token"] == "mock-refresh-token"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_config_non_unique_profile(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
hass_client_no_auth: ClientSessionGenerator,
|
||||||
|
current_request_with_host: None,
|
||||||
|
monzo: AsyncMock,
|
||||||
|
polling_config_entry: MockConfigEntry,
|
||||||
|
aioclient_mock: AiohttpClientMocker,
|
||||||
|
) -> None:
|
||||||
|
"""Test setup a non-unique profile."""
|
||||||
|
await setup_integration(hass, polling_config_entry)
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
state = config_entry_oauth2_flow._encode_jwt(
|
||||||
|
hass,
|
||||||
|
{
|
||||||
|
"flow_id": result["flow_id"],
|
||||||
|
"redirect_uri": "https://example.com/auth/external/callback",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == FlowResultType.EXTERNAL_STEP
|
||||||
|
assert result["url"] == (
|
||||||
|
f"{OAUTH2_AUTHORIZE}/?"
|
||||||
|
f"response_type=code&client_id={CLIENT_ID}&"
|
||||||
|
"redirect_uri=https://example.com/auth/external/callback&"
|
||||||
|
f"state={state}"
|
||||||
|
)
|
||||||
|
|
||||||
|
client = await hass_client_no_auth()
|
||||||
|
resp = await client.get(f"/auth/external/callback?code=abcd&state={state}")
|
||||||
|
assert resp.status == 200
|
||||||
|
assert resp.headers["content-type"] == "text/html; charset=utf-8"
|
||||||
|
|
||||||
|
aioclient_mock.clear_requests()
|
||||||
|
aioclient_mock.post(
|
||||||
|
OAUTH2_TOKEN,
|
||||||
|
json={
|
||||||
|
"refresh_token": "mock-refresh-token",
|
||||||
|
"access_token": "mock-access-token",
|
||||||
|
"type": "Bearer",
|
||||||
|
"expires_in": 60,
|
||||||
|
"user_id": str(USER_ID),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||||
|
assert result["type"] == FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "already_configured"
|
141
tests/components/monzo/test_sensor.py
Normal file
141
tests/components/monzo/test_sensor.py
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
"""Tests for the Monzo component."""
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
from typing import Any
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
from freezegun.api import FrozenDateTimeFactory
|
||||||
|
import pytest
|
||||||
|
from syrupy import SnapshotAssertion
|
||||||
|
|
||||||
|
from homeassistant.components.monzo.const import DOMAIN
|
||||||
|
from homeassistant.components.monzo.sensor import (
|
||||||
|
ACCOUNT_SENSORS,
|
||||||
|
POT_SENSORS,
|
||||||
|
MonzoSensorEntityDescription,
|
||||||
|
)
|
||||||
|
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||||
|
from homeassistant.const import STATE_UNAVAILABLE
|
||||||
|
from homeassistant.core import HomeAssistant, State
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
from homeassistant.helpers.entity_registry import EntityRegistry
|
||||||
|
|
||||||
|
from . import setup_integration
|
||||||
|
from .conftest import TEST_ACCOUNTS, TEST_POTS
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||||
|
from tests.typing import ClientSessionGenerator
|
||||||
|
|
||||||
|
EXPECTED_VALUE_GETTERS = {
|
||||||
|
"balance": lambda x: x["balance"]["balance"] / 100,
|
||||||
|
"total_balance": lambda x: x["balance"]["total_balance"] / 100,
|
||||||
|
"pot_balance": lambda x: x["balance"] / 100,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_entity_id(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
acc_id: str,
|
||||||
|
description: MonzoSensorEntityDescription,
|
||||||
|
) -> str | None:
|
||||||
|
"""Get an entity id for a user's attribute."""
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
unique_id = f"{acc_id}_{description.key}"
|
||||||
|
|
||||||
|
return entity_registry.async_get_entity_id(SENSOR_DOMAIN, DOMAIN, unique_id)
|
||||||
|
|
||||||
|
|
||||||
|
def async_assert_state_equals(
|
||||||
|
entity_id: str,
|
||||||
|
state_obj: State,
|
||||||
|
expected: Any,
|
||||||
|
description: MonzoSensorEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Assert at given state matches what is expected."""
|
||||||
|
assert state_obj, f"Expected entity {entity_id} to exist but it did not"
|
||||||
|
|
||||||
|
assert state_obj.state == str(expected), (
|
||||||
|
f"Expected {expected} but was {state_obj.state} "
|
||||||
|
f"for measure {description.name}, {entity_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||||
|
async def test_sensor_default_enabled_entities(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
monzo: AsyncMock,
|
||||||
|
polling_config_entry: MockConfigEntry,
|
||||||
|
hass_client_no_auth: ClientSessionGenerator,
|
||||||
|
) -> None:
|
||||||
|
"""Test entities enabled by default."""
|
||||||
|
await setup_integration(hass, polling_config_entry)
|
||||||
|
entity_registry: EntityRegistry = er.async_get(hass)
|
||||||
|
|
||||||
|
for acc in TEST_ACCOUNTS:
|
||||||
|
for sensor_description in ACCOUNT_SENSORS:
|
||||||
|
entity_id = await async_get_entity_id(hass, acc["id"], sensor_description)
|
||||||
|
assert entity_id
|
||||||
|
assert entity_registry.async_is_registered(entity_id)
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.state == str(
|
||||||
|
EXPECTED_VALUE_GETTERS[sensor_description.key](acc)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||||
|
async def test_unavailable_entity(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
basic_monzo: AsyncMock,
|
||||||
|
polling_config_entry: MockConfigEntry,
|
||||||
|
hass_client_no_auth: ClientSessionGenerator,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Test entities enabled by default."""
|
||||||
|
await setup_integration(hass, polling_config_entry)
|
||||||
|
basic_monzo.user_account.pots.return_value = [{"id": "pot_savings"}]
|
||||||
|
freezer.tick(timedelta(minutes=100))
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
entity_id = await async_get_entity_id(hass, TEST_POTS[0]["id"], POT_SENSORS[0])
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.state == "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||||
|
async def test_all_entities(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
monzo: AsyncMock,
|
||||||
|
polling_config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test all entities."""
|
||||||
|
await setup_integration(hass, polling_config_entry)
|
||||||
|
|
||||||
|
for acc in TEST_ACCOUNTS:
|
||||||
|
for sensor in ACCOUNT_SENSORS:
|
||||||
|
entity_id = await async_get_entity_id(hass, acc["id"], sensor)
|
||||||
|
assert hass.states.get(entity_id) == snapshot
|
||||||
|
|
||||||
|
|
||||||
|
async def test_update_failed(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
monzo: AsyncMock,
|
||||||
|
polling_config_entry: MockConfigEntry,
|
||||||
|
freezer: FrozenDateTimeFactory,
|
||||||
|
) -> None:
|
||||||
|
"""Test all entities."""
|
||||||
|
await setup_integration(hass, polling_config_entry)
|
||||||
|
|
||||||
|
monzo.user_account.accounts.side_effect = Exception
|
||||||
|
freezer.tick(timedelta(minutes=10))
|
||||||
|
async_fire_time_changed(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
entity_id = await async_get_entity_id(
|
||||||
|
hass, TEST_ACCOUNTS[0]["id"], ACCOUNT_SENSORS[0]
|
||||||
|
)
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state is not None
|
||||||
|
assert state.state == STATE_UNAVAILABLE
|
Loading…
x
Reference in New Issue
Block a user