From 8a17728ab6c1d31a212e47b3f4f30f0d8b1167a6 Mon Sep 17 00:00:00 2001 From: Jay Oswald Date: Wed, 6 Dec 2023 22:32:40 +1100 Subject: [PATCH] initial, potentially working version --- .gitignore | 5 ++ custom_components/up/__init__.py | 21 +++++ custom_components/up/config_flow.py | 39 +++++++++ custom_components/up/const.py | 1 + custom_components/up/manifest.json | 10 +++ custom_components/up/sensor.py | 97 +++++++++++++++++++++++ custom_components/up/strings.json | 13 +++ custom_components/up/translations/en.json | 13 +++ custom_components/up/up.py | 49 ++++++++++++ docker-compose.yml | 13 +++ 10 files changed, 261 insertions(+) create mode 100644 .gitignore create mode 100644 custom_components/up/__init__.py create mode 100644 custom_components/up/config_flow.py create mode 100644 custom_components/up/const.py create mode 100644 custom_components/up/manifest.json create mode 100644 custom_components/up/sensor.py create mode 100644 custom_components/up/strings.json create mode 100644 custom_components/up/translations/en.json create mode 100644 custom_components/up/up.py create mode 100644 docker-compose.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fb48aed --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +#HA Config folder +config/ + +#PY Cache +custom_components/up/__pycache__/* \ No newline at end of file diff --git a/custom_components/up/__init__.py b/custom_components/up/__init__.py new file mode 100644 index 0000000..0259fcd --- /dev/null +++ b/custom_components/up/__init__.py @@ -0,0 +1,21 @@ +import logging +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from .up import UP +from .const import DOMAIN +from homeassistant.const import CONF_API_KEY + +PLATFORMS: list[str] = ["sensor"] +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = UP(entry.data[CONF_API_KEY]) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok \ No newline at end of file diff --git a/custom_components/up/config_flow.py b/custom_components/up/config_flow.py new file mode 100644 index 0000000..490741b --- /dev/null +++ b/custom_components/up/config_flow.py @@ -0,0 +1,39 @@ +import logging +from typing import Any +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY +import voluptuous as vol +from .const import DOMAIN +from .up import UP + +DATA_SCHEMA = vol.Schema({ + vol.Required(CONF_API_KEY): str, + }) +_LOGGER = logging.getLogger(__name__) + +class UpConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + async def async_step_user(self, user_input: dict[str, Any] | None = None): + errors = {} + if user_input is not None: + api_key = user_input[CONF_API_KEY] + try: + up = UP(api_key) + info = await up.test(api_key) + + if info: + return self.async_create_entry(title="UP", data=user_input) + else: + errors[CONF_API_KEY] = "API key failed validation" + except ConnectionError: + _LOGGER.exception("Connection Error") + errors[CONF_API_KEY] = "API Connection Error" + except Exception: + _LOGGER.exception("Unexpected exception") + errors[CONF_API_KEY] = "API Key not validated, unknown error" + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + diff --git a/custom_components/up/const.py b/custom_components/up/const.py new file mode 100644 index 0000000..712d227 --- /dev/null +++ b/custom_components/up/const.py @@ -0,0 +1 @@ +DOMAIN = "up" \ No newline at end of file diff --git a/custom_components/up/manifest.json b/custom_components/up/manifest.json new file mode 100644 index 0000000..e477077 --- /dev/null +++ b/custom_components/up/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "up", + "name": "Up Bank Integration", + "dependencies": ["http"], + "codeowners": ["@jay-oswald"], + "version": "0.0.1", + "integration_type": "hub", + "config_flow": true, + "iot_class": "cloud_polling" +} \ No newline at end of file diff --git a/custom_components/up/sensor.py b/custom_components/up/sensor.py new file mode 100644 index 0000000..db304bd --- /dev/null +++ b/custom_components/up/sensor.py @@ -0,0 +1,97 @@ +import logging +from datetime import timedelta +import async_timeout +from homeassistant.components.number import ( + NumberEntity, + NumberDeviceClass +) +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) +from homeassistant.core import callback + +from .const import DOMAIN +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry(hass, config_entry, async_add_entities): + up = hass.data[DOMAIN][config_entry.entry_id] + + coordinator = UpCoordinator(hass, up) + + await coordinator.async_config_entry_first_refresh() + + async_add_entities(Account(coordinator, coordinator.data[account]) for account in coordinator.data) + +class UpCoordinator(DataUpdateCoordinator): + + def __init__(self, hass, api): + super().__init__( + hass, + _LOGGER, + name="UP Coordinator", + update_interval = timedelta(hours=1) + ) + + self.api = api + + async def _async_update_data(self): + try: + async with async_timeout.timeout(20): + return await self.api.getAccounts() + except Exception as err: + raise UpdateFailed(f"Error communicating with API: {err}") + + + +class Account(CoordinatorEntity, NumberEntity): + account = {} + def __init__(self, coordinator, account): + super().__init__(coordinator, context=account) + self.setValues(account) + + def setValues(self, account): + _LOGGER.warning(account) + self.account = account + self._attr_unique_id = "up_" + account.id + self._attr_name = account.name + self.balance = account.balance + self._state = account.balance + self.id = account.id + + @callback + def _handle_coordinator_update(self) -> None: + self.setValues(self.coordinator.data[self.id]) + self.async_write_ha_state() + + @property + def device_class(self): + return NumberDeviceClass.MONETARY + + @property + def device_info(self): + return { + "identifiers": {(DOMAIN, self._attr_unique_id)}, + "name": self.account.name, + "manufacturer": self.account.ownership, + "model": self.account.accountType + } + + @property + def available(self) -> bool: + return True + + @property + def state(self): + return self.balance + + @property + def mode(self): + return 'box' + + @property + def native_step(self): + return 0.01 + + diff --git a/custom_components/up/strings.json b/custom_components/up/strings.json new file mode 100644 index 0000000..3d20bb5 --- /dev/null +++ b/custom_components/up/strings.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "title": "Config for Up Integration", + "description": "Enter the API key for UP", + "data": { + "api_key": "API Key" + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/up/translations/en.json b/custom_components/up/translations/en.json new file mode 100644 index 0000000..3d20bb5 --- /dev/null +++ b/custom_components/up/translations/en.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "title": "Config for Up Integration", + "description": "Enter the API key for UP", + "data": { + "api_key": "API Key" + } + } + } + } +} \ No newline at end of file diff --git a/custom_components/up/up.py b/custom_components/up/up.py new file mode 100644 index 0000000..b3efdc7 --- /dev/null +++ b/custom_components/up/up.py @@ -0,0 +1,49 @@ +import aiohttp +import logging +_LOGGER = logging.getLogger(__name__) + +base = "https://api.up.com.au/api/v1" + +class UP: + api_key = ""; + def __init__(self, key): + self.api_key = key; + + async def call(self, endpoint, data = {}, method="get"): + headers = { "Authorization": "Bearer " + self.api_key} + match method: + case "get": + async with aiohttp.ClientSession(headers=headers) as session: + async with session.get(base + endpoint) as resp: + resp.data = await resp.json() + return resp + + + + async def test(self, api_key) -> bool: + self.api_key = api_key + result = await self.call("/util/ping") + + return result.status == 200 + + async def getAccounts(self): + result = await self.call('/accounts') + if(result.status != 200): + return False + + accounts = {}; + for account in result.data['data']: + details = BankAccount(account) + accounts[details.id] = details + + return accounts + + +class BankAccount: + def __init__(self, data): + self.name = data['attributes']['displayName'] + self.balance = data['attributes']['balance']['value'] + self.id = data['id'] + self.createdAt = data['attributes']['createdAt'] + self.accountType = data['attributes']['accountType'] + self.ownership = data['attributes']['ownershipType'] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..62e7fdc --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +version: '3' +services: + homeassistant: + container_name: homeassistant + image: "ghcr.io/home-assistant/home-assistant:stable" + volumes: + - ./config:/config + - /etc/localtime:/etc/localtime:ro + - /run/dbus:/run/dbus:ro + - ./custom_components/up:/config/custom_components/up/ + restart: unless-stopped + privileged: true + network_mode: host \ No newline at end of file