diff --git a/README.md b/README.md index 8b8de37..241b3f2 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,18 @@ -[![home-assistant-sbanken](https://img.shields.io/github/release/toringer/home-assistant-sbanken.svg?1)](https://github.com/toringer/home-assistant-sbanken) +[![home-assistant-sbanken](https://img.shields.io/github/release/toringer/home-assistant-sbanken.svg?1)](https://github.com/toringer/home-assistant-sbanken) [![Validate with hassfest](https://github.com/toringer/home-assistant-sbanken/workflows/Validate%20with%20hassfest/badge.svg)](https://github.com/toringer/home-assistant-sbanken/actions/workflows/hassfest.yaml) [![HACS Validation](https://github.com/toringer/home-assistant-sbanken/actions/workflows/validate_hacs.yaml/badge.svg)](https://github.com/toringer/home-assistant-sbanken/actions/workflows/validate_hacs.yaml) [![Maintenance](https://img.shields.io/maintenance/yes/2022.svg)](https://github.com/toringer/home-assistant-sbanken) -[![home-assistant-sbanken_downloads](https://img.shields.io/github/downloads/toringer/home-assistant-sbanken/total)](https://github.com/toringer/home-assistant-sbanken) +[![home-assistant-sbanken_downloads](https://img.shields.io/github/downloads/toringer/home-assistant-sbanken/total)](https://github.com/toringer/home-assistant-sbanken) [![home-assistant-sbanken_downloads](https://img.shields.io/github/downloads/toringer/home-assistant-sbanken/latest/total)](https://github.com/toringer/home-assistant-sbanken) +# Sbanken sensor platform for Home Assistant +Get your Sbanken account information integrated into your Home Assistant. The Sbanken integration will create an entity for each of your bank accounts. The entity displays current available amount, transactions, payments and other information. +The integration adds two services -# Sbanken sensor platform for Home Assistant -Get your Sbanken account information integrated into your Home Assistant. The Sbanken integration will create an entity for each of your bank accounts. The entity displays current available amount, transactions, payments and other information. The integration also adds a service `sbanken.transfer` that allows you to transfer amounts between your accounts. +- `sbanken.transfer` to transfer amounts between your accounts. +- `sbanken.update_account` to force an account information update ![Header](https://github.com/toringer/home-assistant-sbanken/blob/master/accounts.png) diff --git a/custom_components/sbanken/__init__.py b/custom_components/sbanken/__init__.py index 75262d6..071abe3 100644 --- a/custom_components/sbanken/__init__.py +++ b/custom_components/sbanken/__init__.py @@ -1,13 +1,11 @@ """The SBanken integration.""" from __future__ import annotations import logging -import voluptuous as vol -from .sbankenApi import SbankenApi -from .services import async_setup_services from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from .const import DOMAIN, CONF_CLIENT_ID, CONF_SECRET, CONF_NUMBER_OF_TRANSACTIONS +from .sbanken_api import SbankenApi +from .const import DOMAIN, CONF_CLIENT_ID, CONF_SECRET _LOGGER = logging.getLogger(__name__) PLATFORMS: list[Platform] = [Platform.SENSOR] @@ -23,7 +21,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: api = SbankenApi(entry.options.get(CONF_CLIENT_ID), entry.options.get(CONF_SECRET)) hass.data[DOMAIN]["api"] = api hass.data[DOMAIN]["listener"] = entry.add_update_listener(update_listener) - await async_setup_services(hass) return True diff --git a/custom_components/sbanken/config_flow.py b/custom_components/sbanken/config_flow.py index 4e5969e..da9dfdb 100644 --- a/custom_components/sbanken/config_flow.py +++ b/custom_components/sbanken/config_flow.py @@ -1,14 +1,13 @@ """Config flow for Sbanken integration.""" from __future__ import annotations import logging -import voluptuous as vol from typing import Any +import voluptuous as vol from homeassistant import config_entries from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult -from homeassistant.exceptions import HomeAssistantError from homeassistant.core import callback -from .sbankenApi import SbankenApi +from .sbanken_api import SbankenApi from .const import ( DOMAIN, CONF_CLIENT_ID, diff --git a/custom_components/sbanken/const.py b/custom_components/sbanken/const.py index 4992e50..47e7efc 100644 --- a/custom_components/sbanken/const.py +++ b/custom_components/sbanken/const.py @@ -2,7 +2,6 @@ from homeassistant.exceptions import HomeAssistantError - DOMAIN = "sbanken" TITLE = "Sbanken" CONF_CLIENT_ID = "client_id" @@ -19,6 +18,11 @@ ATTR_TRANSACTIONS = "transactions" ATTR_PAYMENTS = "payments" ATTR_STANDING_ORDERS = "standing_orders" +ATTR_AMOUNT = "amount" +ATTR_FROM_ACCOUNT_ENTITY = "from_account_entity" +ATTR_TO_ACCOUNT_ENTITY = "to_account_entity" +ATTR_MESSAGE = "message" +ATTR_UPDATE_ACCOUNT_ENTITY = "update_account_entity" class InvalidAuth(HomeAssistantError): diff --git a/custom_components/sbanken/manifest.json b/custom_components/sbanken/manifest.json index a2fde3e..4521a0b 100644 --- a/custom_components/sbanken/manifest.json +++ b/custom_components/sbanken/manifest.json @@ -1,7 +1,7 @@ { "domain": "sbanken", "name": "Sbanken", - "version": "2.0.9", + "version": "2.1.0", "config_flow": true, "documentation": "https://github.com/toringer/home-assistant-sbanken", "issue_tracker": "https://github.com/toringer/home-assistant-sbanken/issues", diff --git a/custom_components/sbanken/sbankenApi.py b/custom_components/sbanken/sbanken_api.py similarity index 95% rename from custom_components/sbanken/sbankenApi.py rename to custom_components/sbanken/sbanken_api.py index a452dda..8d3177d 100644 --- a/custom_components/sbanken/sbankenApi.py +++ b/custom_components/sbanken/sbanken_api.py @@ -23,12 +23,9 @@ def __init__(self, client_id: string, secret: string) -> None: def get_session(self) -> OAuth2Session: """Get session""" - if ( - self._session is None - or self._session.authorized is False - or self._session_expires_at < datetime.now() - ): - _LOGGER.info("Create new session") + _LOGGER.debug("Get session") + if self._session is None or self._session_expires_at < datetime.now(): + _LOGGER.debug("Create new session") self._session = self._create_session(self.client_id, self.secret) return self._session diff --git a/custom_components/sbanken/sbanken_entities.py b/custom_components/sbanken/sbanken_entities.py new file mode 100644 index 0000000..578c75b --- /dev/null +++ b/custom_components/sbanken/sbanken_entities.py @@ -0,0 +1,176 @@ +""" Sbanken entities""" +import logging +from datetime import datetime +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorStateClass, +) +from homeassistant.helpers.entity import Entity +from .sbanken_api import SbankenApi +from .const import ( + ATTR_ACCOUNT_ID, + ATTR_ACCOUNT_LIMIT, + ATTR_ACCOUNT_NUMBER, + ATTR_ACCOUNT_TYPE, + ATTR_AVAILABLE, + ATTR_BALANCE, + ATTR_LAST_UPDATE, + ATTR_NAME, + ATTR_PAYMENTS, + ATTR_STANDING_ORDERS, + ATTR_TRANSACTIONS, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + + +class SbankenAccountSensor(Entity): + """Representation of a Sensor.""" + + def __init__( + self, + account, + api: SbankenApi, + number_of_transactions: int, + hass: HomeAssistant, + customer_info, + ) -> None: + """Initialize the sensor.""" + self.api = api + self.number_of_transactions = number_of_transactions + self.hass = hass + self.customer_info = customer_info + self._account = account + self._account_id = account["accountId"] + self._transactions = [] + self._payments = [] + self._standing_orders = [] + self._state = float(account["available"]) + self._attr_state_class = SensorStateClass.MEASUREMENT + self._attr_device_class = SensorDeviceClass.MONETARY + self._attr_unique_id = self._account["accountNumber"] + self._attr_name = f"{self._account['name']} ({self._account['accountNumber']})" + self._attr_unit_of_measurement = "NOK" + self._attr_icon = "mdi:cash" + + @property + def device_info(self): + """Return the device_info of the device.""" + device_info = DeviceInfo( + identifiers={(DOMAIN, self.customer_info["customerId"])}, + name=f"Sbanken: {self.customer_info['firstName']} {self.customer_info['lastName']}", + manufacturer="Sbanken", + configuration_url="https://sbanken.no/", + ) + return device_info + + @property + def state(self) -> float: + """Return the state of the sensor.""" + return self._state + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_ACCOUNT_ID: self._account_id, + ATTR_AVAILABLE: self._account["available"], + ATTR_BALANCE: self._account["balance"], + ATTR_ACCOUNT_NUMBER: self._account["accountNumber"], + ATTR_NAME: self._account["name"], + ATTR_ACCOUNT_TYPE: self._account["accountType"], + ATTR_ACCOUNT_LIMIT: self._account["creditLimit"], + ATTR_LAST_UPDATE: datetime.now().strftime("%d/%m/%Y %H:%M:%S"), + ATTR_TRANSACTIONS: self._transactions, + ATTR_PAYMENTS: self._payments, + ATTR_STANDING_ORDERS: self._standing_orders, + } + + async def async_update(self): + """Fetch new state data for the sensor. + + This is the only method that should fetch new data for Home Assistant. + """ + account = await self.hass.async_add_executor_job( + self.api.get_account, self._account_id + ) + transactions = await self.hass.async_add_executor_job( + self.api.get_transactions, + self._account_id, + self.number_of_transactions, + ) + payments = await self.hass.async_add_executor_job( + self.api.get_payments, + self._account_id, + self.number_of_transactions, + ) + + standing_orders = await self.hass.async_add_executor_job( + self.api.get_standing_orders, self._account_id + ) + + self._transactions = transactions + self._payments = payments + self._account = account + self._standing_orders = standing_orders + self._state = float(account["available"]) + _LOGGER.debug(f"Updating Sbanken Sensors: {self._attr_name}") + + +class CustomerInformationSensor(Entity): + """Representation of a Sensor.""" + + def __init__(self, api: SbankenApi, hass: HomeAssistant, customer_info) -> None: + """Initialize the sensor.""" + self.api = api + self.hass = hass + self.customer_info = customer_info + self._state = ( + f"{self.customer_info['firstName']} {self.customer_info['lastName']}" + ) + self._attr_unique_id = self.customer_info["customerId"] + self._attr_name = "Customer information" + self._attr_icon = "mdi:information-outline" + + @property + def device_info(self): + """Return the device_info of the device.""" + device_info = DeviceInfo( + identifiers={(DOMAIN, self.customer_info["customerId"])}, + name=f"Sbanken: {self.customer_info['firstName']} {self.customer_info['lastName']}", + manufacturer="Sbanken", + configuration_url="https://sbanken.no/", + ) + return device_info + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + return { + "customerId": self.customer_info["customerId"], + "firstName": self.customer_info["firstName"], + "lastName": self.customer_info["lastName"], + "emailAddress": self.customer_info["emailAddress"], + "dateOfBirth": self.customer_info["dateOfBirth"], + "postalAddress": self.customer_info["postalAddress"], + "streetAddress": self.customer_info["streetAddress"], + "phoneNumbers": self.customer_info["phoneNumbers"], + } + + async def async_update(self): + """Fetch new state data for the sensor. + + This is the only method that should fetch new data for Home Assistant. + """ + self.customer_info = await self.hass.async_add_executor_job( + self.api.get_customer_information + ) + _LOGGER.debug(f"Updating Sbanken Sensors: {self._attr_name}") diff --git a/custom_components/sbanken/sensor.py b/custom_components/sbanken/sensor.py index d760826..7b8e674 100644 --- a/custom_components/sbanken/sensor.py +++ b/custom_components/sbanken/sensor.py @@ -1,32 +1,14 @@ """Sbanken accounts sensor.""" import logging -from datetime import datetime, timedelta +from datetime import timedelta from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.entity import DeviceInfo -from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorStateClass, -) -from homeassistant.helpers.entity import Entity -from .sbankenApi import SbankenApi -from .const import ( - CONF_NUMBER_OF_TRANSACTIONS, - ATTR_ACCOUNT_ID, - ATTR_ACCOUNT_LIMIT, - ATTR_ACCOUNT_NUMBER, - ATTR_ACCOUNT_TYPE, - ATTR_AVAILABLE, - ATTR_BALANCE, - ATTR_LAST_UPDATE, - ATTR_NAME, - ATTR_PAYMENTS, - ATTR_STANDING_ORDERS, - ATTR_TRANSACTIONS, - DOMAIN, -) +from .services import async_setup_services +from .sbanken_api import SbankenApi +from .sbanken_entities import SbankenAccountSensor, CustomerInformationSensor +from .const import CONF_NUMBER_OF_TRANSACTIONS, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -41,149 +23,15 @@ async def async_setup_entry( accounts = await hass.async_add_executor_job(api.get_accounts) customer_info = await hass.async_add_executor_job(api.get_customer_information) sensors = [ - SbankenAccountSensor(account, api, entry.options, hass, customer_info) + SbankenAccountSensor( + account, + api, + entry.options.get(CONF_NUMBER_OF_TRANSACTIONS), + hass, + customer_info, + ) for account in accounts ] sensors.append(CustomerInformationSensor(api, hass, customer_info)) async_add_entities(sensors, update_before_add=True) - - -class SbankenAccountSensor(Entity): - """Representation of a Sensor.""" - - def __init__( - self, account, api: SbankenApi, options, hass: HomeAssistant, customer_info - ) -> None: - """Initialize the sensor.""" - self.api = api - self.number_of_transactions = options.get(CONF_NUMBER_OF_TRANSACTIONS) # TODO - self.hass = hass - self.customer_info = customer_info - self._account = account - self._transactions = [] - self._payments = [] - self._standing_orders = [] - self._state = float(account["available"]) - self._attr_state_class = SensorStateClass.MEASUREMENT - self._attr_device_class = SensorDeviceClass.MONETARY - self._attr_unique_id = self._account["accountNumber"] - self._attr_name = f"{self._account['name']} ({self._account['accountNumber']})" - self._attr_unit_of_measurement = "NOK" - self._attr_icon = "mdi:cash" - - @property - def device_info(self): - """Return the device_info of the device.""" - device_info = DeviceInfo( - identifiers={(DOMAIN, self.customer_info["customerId"])}, - name=f"Sbanken: {self.customer_info['firstName']} {self.customer_info['lastName']}", - manufacturer="Sbanken", - ) - return device_info - - @property - def state(self) -> float: - """Return the state of the sensor.""" - return self._state - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return { - ATTR_ACCOUNT_ID: self._account["accountId"], - ATTR_AVAILABLE: self._account["available"], - ATTR_BALANCE: self._account["balance"], - ATTR_ACCOUNT_NUMBER: self._account["accountNumber"], - ATTR_NAME: self._account["name"], - ATTR_ACCOUNT_TYPE: self._account["accountType"], - ATTR_ACCOUNT_LIMIT: self._account["creditLimit"], - ATTR_LAST_UPDATE: datetime.now().strftime("%d/%m/%Y %H:%M:%S"), - ATTR_TRANSACTIONS: self._transactions, - ATTR_PAYMENTS: self._payments, - ATTR_STANDING_ORDERS: self._standing_orders, - } - - async def async_update(self): - """Fetch new state data for the sensor. - - This is the only method that should fetch new data for Home Assistant. - """ - account = await self.hass.async_add_executor_job( - self.api.get_account, self._account["accountId"] - ) - transactions = await self.hass.async_add_executor_job( - self.api.get_transactions, - self._account["accountId"], - self.number_of_transactions, - ) - payments = await self.hass.async_add_executor_job( - self.api.get_payments, - self._account["accountId"], - self.number_of_transactions, - ) - - standing_orders = await self.hass.async_add_executor_job( - self.api.get_standing_orders, self._account["accountId"] - ) - - self._transactions = transactions - self._payments = payments - self._account = account - self._standing_orders = standing_orders - self._state = float(account["available"]) - _LOGGER.debug(f"Updating Sbanken Sensors: {self._attr_name}") - - -class CustomerInformationSensor(Entity): - """Representation of a Sensor.""" - - def __init__(self, api: SbankenApi, hass: HomeAssistant, customer_info) -> None: - """Initialize the sensor.""" - self.api = api - self.hass = hass - self.customer_info = customer_info - self._state = ( - f"{self.customer_info['firstName']} {self.customer_info['lastName']}" - ) - self._attr_unique_id = self.customer_info["customerId"] - self._attr_name = "Customer information" - self._attr_icon = "mdi:information-outline" - - @property - def device_info(self): - """Return the device_info of the device.""" - device_info = DeviceInfo( - identifiers={(DOMAIN, self.customer_info["customerId"])}, - name=f"Sbanken: {self.customer_info['firstName']} {self.customer_info['lastName']}", - manufacturer="Sbanken", - ) - return device_info - - @property - def state(self): - """Return the state of the sensor.""" - return self._state - - @property - def extra_state_attributes(self): - """Return the state attributes.""" - return { - "customerId": self.customer_info["customerId"], - "firstName": self.customer_info["firstName"], - "lastName": self.customer_info["lastName"], - "emailAddress": self.customer_info["emailAddress"], - "dateOfBirth": self.customer_info["dateOfBirth"], - "postalAddress": self.customer_info["postalAddress"], - "streetAddress": self.customer_info["streetAddress"], - "phoneNumbers": self.customer_info["phoneNumbers"], - } - - async def async_update(self): - """Fetch new state data for the sensor. - - This is the only method that should fetch new data for Home Assistant. - """ - self.customer_info = await self.hass.async_add_executor_job( - self.api.get_customer_information - ) - _LOGGER.debug(f"Updating Sbanken Sensors: {self._attr_name}") + await async_setup_services(hass, sensors) diff --git a/custom_components/sbanken/services.py b/custom_components/sbanken/services.py index d1addef..85de82c 100644 --- a/custom_components/sbanken/services.py +++ b/custom_components/sbanken/services.py @@ -1,18 +1,24 @@ """ Sbanken services.""" +from datetime import timedelta import logging - import voluptuous as vol from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv -from .sbankenApi import SbankenApi -from .const import DOMAIN, ATTR_ACCOUNT_ID +from homeassistant.helpers.event import async_call_later +from .sbanken_api import SbankenApi +from .sbanken_entities import SbankenAccountSensor +from .const import ( + DOMAIN, + ATTR_ACCOUNT_ID, + ATTR_FROM_ACCOUNT_ENTITY, + ATTR_TO_ACCOUNT_ENTITY, + ATTR_AMOUNT, + ATTR_MESSAGE, + ATTR_UPDATE_ACCOUNT_ENTITY, +) _LOGGER = logging.getLogger(__name__) -ATTR_AMOUNT = "amount" -ATTR_FROM_ACCOUNT_ENTITY = "from_account_entity" -ATTR_TO_ACCOUNT_ENTITY = "to_account_entity" -ATTR_MESSAGE = "message" SERVICE_TRANSFER_SCHEMA = vol.Schema( @@ -24,18 +30,25 @@ } ) +SERVICE_UPDATE_ACCOUNT_SCHEMA = vol.Schema( + { + vol.Required(ATTR_UPDATE_ACCOUNT_ENTITY): cv.string, + } +) + -async def async_setup_services(hass: HomeAssistant): +async def async_setup_services( + hass: HomeAssistant, entities: list[SbankenAccountSensor] +): """Setup services for Sbanken.""" async def transfer_service(call): # pylint: disable=possibly-unused-variable """Execute a service to Sbanken.""" - _LOGGER.debug(f"Handle transfers: {str(call.data)}") - from_account_entity = call.data.get(ATTR_FROM_ACCOUNT_ENTITY) to_account_entity = call.data.get(ATTR_TO_ACCOUNT_ENTITY) amount = call.data.get(ATTR_AMOUNT) message = call.data.get(ATTR_MESSAGE) + _LOGGER.debug(f"Transfer amount: {str(amount)}") if ATTR_ACCOUNT_ID not in hass.states.get(from_account_entity).attributes: raise HomeAssistantError( @@ -52,12 +65,37 @@ async def transfer_service(call): # pylint: disable=possibly-unused-variable to_account_id = hass.states.get(to_account_entity).attributes[ATTR_ACCOUNT_ID] api: SbankenApi = hass.data[DOMAIN]["api"] - transfer = await hass.async_add_executor_job( + await hass.async_add_executor_job( api.transfer, from_account_id, to_account_id, amount, message ) - return transfer + _LOGGER.debug("Schedule entity update in 5 seconds") + + async def refresh_callback(_): + for entity in entities: + if ( + entity.entity_id == from_account_entity + or entity.entity_id == to_account_entity + ): + await entity.async_update() + entity.async_schedule_update_ha_state() + + async_call_later(hass, timedelta(seconds=5), refresh_callback) + + async def update_account(call): # pylint: disable=possibly-unused-variable + """Execute a service to Sbanken.""" + update_account_entity = call.data.get(ATTR_UPDATE_ACCOUNT_ENTITY) + _LOGGER.debug(f"Update account: {update_account_entity}") + + for entity in entities: + if entity.entity_id == update_account_entity: + await entity.async_update() + entity.async_schedule_update_ha_state() + break hass.services.async_register( DOMAIN, "transfer", transfer_service, schema=SERVICE_TRANSFER_SCHEMA ) + hass.services.async_register( + DOMAIN, "update_account", update_account, schema=SERVICE_UPDATE_ACCOUNT_SCHEMA + ) diff --git a/custom_components/sbanken/services.yaml b/custom_components/sbanken/services.yaml index e013783..e7ee493 100644 --- a/custom_components/sbanken/services.yaml +++ b/custom_components/sbanken/services.yaml @@ -1,3 +1,16 @@ +update_account: + name: Update account + description: "Update account information." + fields: + update_account_entity: + name: Account + description: The account to update + example: 123456 + required: true + selector: + entity: + integration: sbanken + device_class: monetary transfer: name: "Transfer" description: "Transfer between accounts." @@ -10,6 +23,7 @@ transfer: selector: entity: integration: sbanken + device_class: monetary to_account_entity: name: "To account" description: The account to transfer the amount to @@ -18,6 +32,7 @@ transfer: selector: entity: integration: sbanken + device_class: monetary amount: name: "Amount" description: The amount to transfer