Add N26 component (#22684)

* upgraded n26 dependency
removed card_id config parameter (unnecessary)
get_token is now a public method inside n26 dependency

* Add manifest.json
This commit is contained in:
Markus Ressel 2019-04-04 19:18:54 +02:00 committed by Jason Hu
parent 9bb88a6143
commit 07d739c14e
7 changed files with 486 additions and 0 deletions

View File

@ -366,6 +366,7 @@ omit =
homeassistant/components/mystrom/binary_sensor.py
homeassistant/components/mystrom/light.py
homeassistant/components/mystrom/switch.py
homeassistant/components/n26/*
homeassistant/components/nad/media_player.py
homeassistant/components/nadtcp/media_player.py
homeassistant/components/nanoleaf/light.py

View File

@ -0,0 +1,153 @@
"""Support for N26 bank accounts."""
from datetime import datetime, timedelta, timezone
import logging
import voluptuous as vol
from homeassistant.const import (
CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import load_platform
from homeassistant.util import Throttle
from .const import DATA, DOMAIN
REQUIREMENTS = ['n26==0.2.7']
_LOGGER = logging.getLogger(__name__)
DEFAULT_SCAN_INTERVAL = timedelta(minutes=30)
# define configuration parameters
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_SCAN_INTERVAL,
default=DEFAULT_SCAN_INTERVAL): cv.time_period,
}),
}, extra=vol.ALLOW_EXTRA)
N26_COMPONENTS = [
'sensor',
'switch'
]
def setup(hass, config):
"""Set up N26 Component."""
user = config[DOMAIN][CONF_USERNAME]
password = config[DOMAIN][CONF_PASSWORD]
from n26 import api, config as api_config
api = api.Api(api_config.Config(user, password))
from requests import HTTPError
try:
api.get_token()
except HTTPError as err:
_LOGGER.error(str(err))
return False
api_data = N26Data(api)
api_data.update()
hass.data[DOMAIN] = {}
hass.data[DOMAIN][DATA] = api_data
# Load components for supported devices
for component in N26_COMPONENTS:
load_platform(hass, component, DOMAIN, {}, config)
return True
def timestamp_ms_to_date(epoch_ms) -> datetime or None:
"""Convert millisecond timestamp to datetime."""
if epoch_ms:
return datetime.fromtimestamp(epoch_ms / 1000, timezone.utc)
class N26Data:
"""Handle N26 API object and limit updates."""
def __init__(self, api):
"""Initialize the data object."""
self._api = api
self._account_info = {}
self._balance = {}
self._limits = {}
self._account_statuses = {}
self._cards = {}
self._spaces = {}
@property
def api(self):
"""Return N26 api client."""
return self._api
@property
def account_info(self):
"""Return N26 account info."""
return self._account_info
@property
def balance(self):
"""Return N26 account balance."""
return self._balance
@property
def limits(self):
"""Return N26 account limits."""
return self._limits
@property
def account_statuses(self):
"""Return N26 account statuses."""
return self._account_statuses
@property
def cards(self):
"""Return N26 cards."""
return self._cards
def card(self, card_id: str, default: dict = None):
"""Return a card by its id or the given default."""
return next((card for card in self.cards if card["id"] == card_id),
default)
@property
def spaces(self):
"""Return N26 spaces."""
return self._spaces
def space(self, space_id: str, default: dict = None):
"""Return a space by its id or the given default."""
return next((space for space in self.spaces["spaces"]
if space["id"] == space_id), default)
@Throttle(min_time=DEFAULT_SCAN_INTERVAL * 0.8)
def update_account(self):
"""Get the latest account data from N26."""
self._account_info = self._api.get_account_info()
self._balance = self._api.get_balance()
self._limits = self._api.get_account_limits()
self._account_statuses = self._api.get_account_statuses()
@Throttle(min_time=DEFAULT_SCAN_INTERVAL * 0.8)
def update_cards(self):
"""Get the latest cards data from N26."""
self._cards = self._api.get_cards()
@Throttle(min_time=DEFAULT_SCAN_INTERVAL * 0.8)
def update_spaces(self):
"""Get the latest spaces data from N26."""
self._spaces = self._api.get_spaces()
def update(self):
"""Get the latest data from N26."""
self.update_account()
self.update_cards()
self.update_spaces()

View File

@ -0,0 +1,7 @@
"""Provides the constants needed for component."""
DOMAIN = "n26"
DATA = "data"
CARD_STATE_ACTIVE = "M_ACTIVE"
CARD_STATE_BLOCKED = "M_DISABLED"

View File

@ -0,0 +1,10 @@
{
"domain": "n26",
"name": "N26",
"documentation": "https://www.home-assistant.io/components/n26",
"requirements": [
"n26==0.2.7"
],
"dependencies": [],
"codeowners": []
}

View File

@ -0,0 +1,248 @@
"""Support for N26 bank account sensors."""
import logging
from homeassistant.helpers.entity import Entity
from . import DEFAULT_SCAN_INTERVAL, DOMAIN, timestamp_ms_to_date
from .const import DATA
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['n26']
SCAN_INTERVAL = DEFAULT_SCAN_INTERVAL
ATTR_IBAN = "account"
ATTR_USABLE_BALANCE = "usable_balance"
ATTR_BANK_BALANCE = "bank_balance"
ATTR_ACC_OWNER_TITLE = "owner_title"
ATTR_ACC_OWNER_FIRST_NAME = "owner_first_name"
ATTR_ACC_OWNER_LAST_NAME = "owner_last_name"
ATTR_ACC_OWNER_GENDER = "owner_gender"
ATTR_ACC_OWNER_BIRTH_DATE = "owner_birth_date"
ATTR_ACC_OWNER_EMAIL = "owner_email"
ATTR_ACC_OWNER_PHONE_NUMBER = "owner_phone_number"
ICON_ACCOUNT = 'mdi:currency-eur'
ICON_CARD = 'mdi:credit-card'
ICON_SPACE = 'mdi:crop-square'
def setup_platform(
hass, config, add_entities, discovery_info=None):
"""Set up the N26 sensor platform."""
api_data = hass.data[DOMAIN][DATA]
sensor_entities = [N26Account(api_data)]
for card in api_data.cards:
sensor_entities.append(N26Card(api_data, card))
for space in api_data.spaces["spaces"]:
sensor_entities.append(N26Space(api_data, space))
add_entities(sensor_entities)
class N26Account(Entity):
"""Sensor for a N26 balance account.
A balance account contains an amount of money (=balance). The amount may
also be negative.
"""
def __init__(self, api_data) -> None:
"""Initialize a N26 balance account."""
self._data = api_data
self._iban = self._data.balance["iban"]
def update(self) -> None:
"""Get the current balance and currency for the account."""
self._data.update_account()
@property
def unique_id(self):
"""Return the unique ID of the entity."""
return self._iban[-4:]
@property
def name(self) -> str:
"""Friendly name of the sensor."""
return "n26_{}".format(self._iban[-4:])
@property
def state(self) -> float:
"""Return the balance of the account as state."""
if self._data.balance is None:
return None
return self._data.balance.get("availableBalance")
@property
def unit_of_measurement(self) -> str:
"""Use the currency as unit of measurement."""
if self._data.balance is None:
return None
return self._data.balance.get("currency")
@property
def device_state_attributes(self) -> dict:
"""Additional attributes of the sensor."""
attributes = {
ATTR_IBAN: self._data.balance.get("iban"),
ATTR_BANK_BALANCE: self._data.balance.get("bankBalance"),
ATTR_USABLE_BALANCE: self._data.balance.get("usableBalance"),
ATTR_ACC_OWNER_TITLE: self._data.account_info.get("title"),
ATTR_ACC_OWNER_FIRST_NAME:
self._data.account_info.get("kycFirstName"),
ATTR_ACC_OWNER_LAST_NAME:
self._data.account_info.get("kycLastName"),
ATTR_ACC_OWNER_GENDER: self._data.account_info.get("gender"),
ATTR_ACC_OWNER_BIRTH_DATE: timestamp_ms_to_date(
self._data.account_info.get("birthDate")),
ATTR_ACC_OWNER_EMAIL: self._data.account_info.get("email"),
ATTR_ACC_OWNER_PHONE_NUMBER:
self._data.account_info.get("mobilePhoneNumber"),
}
for limit in self._data.limits:
limit_attr_name = "limit_{}".format(limit["limit"].lower())
attributes[limit_attr_name] = limit["amount"]
return attributes
@property
def icon(self) -> str:
"""Set the icon for the sensor."""
return ICON_ACCOUNT
class N26Card(Entity):
"""Sensor for a N26 card."""
def __init__(self, api_data, card) -> None:
"""Initialize a N26 card."""
self._data = api_data
self._account_name = api_data.balance["iban"][-4:]
self._card = card
def update(self) -> None:
"""Get the current balance and currency for the account."""
self._data.update_cards()
self._card = self._data.card(self._card["id"], self._card)
@property
def unique_id(self):
"""Return the unique ID of the entity."""
return self._card["id"]
@property
def name(self) -> str:
"""Friendly name of the sensor."""
return "{}_card_{}".format(
self._account_name.lower(), self._card["id"])
@property
def state(self) -> float:
"""Return the balance of the account as state."""
return self._card["status"]
@property
def device_state_attributes(self) -> dict:
"""Additional attributes of the sensor."""
attributes = {
"apple_pay_eligible": self._card.get("applePayEligible"),
"card_activated": timestamp_ms_to_date(
self._card.get("cardActivated")),
"card_product": self._card.get("cardProduct"),
"card_product_type": self._card.get("cardProductType"),
"card_settings_id": self._card.get("cardSettingsId"),
"card_Type": self._card.get("cardType"),
"design": self._card.get("design"),
"exceet_actual_delivery_date":
self._card.get("exceetActualDeliveryDate"),
"exceet_card_status": self._card.get("exceetCardStatus"),
"exceet_expected_delivery_date":
self._card.get("exceetExpectedDeliveryDate"),
"exceet_express_card_delivery":
self._card.get("exceetExpressCardDelivery"),
"exceet_express_card_delivery_email_sent":
self._card.get("exceetExpressCardDeliveryEmailSent"),
"exceet_express_card_delivery_tracking_id":
self._card.get("exceetExpressCardDeliveryTrackingId"),
"expiration_date": timestamp_ms_to_date(
self._card.get("expirationDate")),
"google_pay_eligible": self._card.get("googlePayEligible"),
"masked_pan": self._card.get("maskedPan"),
"membership": self._card.get("membership"),
"mpts_card": self._card.get("mptsCard"),
"pan": self._card.get("pan"),
"pin_defined": timestamp_ms_to_date(self._card.get("pinDefined")),
"username_on_card": self._card.get("usernameOnCard"),
}
return attributes
@property
def icon(self) -> str:
"""Set the icon for the sensor."""
return ICON_CARD
class N26Space(Entity):
"""Sensor for a N26 space."""
def __init__(self, api_data, space) -> None:
"""Initialize a N26 space."""
self._data = api_data
self._space = space
def update(self) -> None:
"""Get the current balance and currency for the account."""
self._data.update_spaces()
self._space = self._data.space(self._space["id"], self._space)
@property
def unique_id(self):
"""Return the unique ID of the entity."""
return "space_{}".format(self._space["name"].lower())
@property
def name(self) -> str:
"""Friendly name of the sensor."""
return self._space["name"]
@property
def state(self) -> float:
"""Return the balance of the account as state."""
return self._space["balance"]["availableBalance"]
@property
def unit_of_measurement(self) -> str:
"""Use the currency as unit of measurement."""
return self._space["balance"]["currency"]
@property
def device_state_attributes(self) -> dict:
"""Additional attributes of the sensor."""
goal_value = ""
if "goal" in self._space:
goal_value = self._space.get("goal").get("amount")
attributes = {
"name": self._space.get("name"),
"goal": goal_value,
"background_image_url": self._space.get("backgroundImageUrl"),
"image_url": self._space.get("imageUrl"),
"is_card_attached": self._space.get("isCardAttached"),
"is_hidden_from_balance": self._space.get("isHiddenFromBalance"),
"is_locked": self._space.get("isLocked"),
"is_primary": self._space.get("isPrimary"),
}
return attributes
@property
def icon(self) -> str:
"""Set the icon for the sensor."""
return ICON_SPACE

View File

@ -0,0 +1,64 @@
"""Support for N26 switches."""
import logging
from homeassistant.components.switch import SwitchDevice
from . import DEFAULT_SCAN_INTERVAL, DOMAIN
from .const import CARD_STATE_ACTIVE, CARD_STATE_BLOCKED, DATA
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['n26']
SCAN_INTERVAL = DEFAULT_SCAN_INTERVAL
def setup_platform(
hass, config, add_entities, discovery_info=None):
"""Set up the N26 switch platform."""
api_data = hass.data[DOMAIN][DATA]
switch_entities = []
for card in api_data.cards:
switch_entities.append(N26CardSwitch(api_data, card))
add_entities(switch_entities)
class N26CardSwitch(SwitchDevice):
"""Representation of a N26 card block/unblock switch."""
def __init__(self, api_data, card: dict):
"""Initialize the N26 card block/unblock switch."""
self._data = api_data
self._card = card
@property
def unique_id(self):
"""Return the unique ID of the entity."""
return self._card["id"]
@property
def name(self) -> str:
"""Friendly name of the sensor."""
return "card_{}".format(self._card["id"])
@property
def is_on(self):
"""Return true if switch is on."""
return self._card["status"] == CARD_STATE_ACTIVE
def turn_on(self, **kwargs):
"""Block the card."""
self._data.api.unblock_card(self._card["id"])
self._card["status"] = CARD_STATE_ACTIVE
def turn_off(self, **kwargs):
"""Unblock the card."""
self._data.api.block_card(self._card["id"])
self._card["status"] = CARD_STATE_BLOCKED
def update(self):
"""Update the switch state."""
self._data.update_cards()
self._card = self._data.card(self._card["id"], self._card)

View File

@ -725,6 +725,9 @@ mycroftapi==2.0
# homeassistant.components.usps
myusps==1.3.2
# homeassistant.components.n26
n26==0.2.7
# homeassistant.components.nad.media_player
nad_receiver==0.0.11