Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions custom_components/ynab_custom/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data[DOMAIN] = {}

hass.data[DOMAIN][entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, ["sensor"])
await hass.config_entries.async_forward_entry_setups(entry, ["sensor", "number"])

# Set up options update listener
entry.async_on_unload(entry.add_update_listener(async_update_options))
Expand Down Expand Up @@ -148,7 +148,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
if coordinator:
await coordinator.async_shutdown()

result = await hass.config_entries.async_unload_platforms(entry, ["sensor"])
result = await hass.config_entries.async_unload_platforms(entry, ["sensor", "number"])

if removed_entities:
_LOGGER.debug(f"Unload complete for {entry.entry_id} - removed {len(removed_entities)} entities")
Expand Down
98 changes: 98 additions & 0 deletions custom_components/ynab_custom/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry, budget_id: str, budg

# Create persistent data key for this budget
self.persistent_data_key = f"ynab_data_{entry.entry_id}"

# Store user-managed credit settings separately from API payload snapshots.
self._user_store = Store(
hass,
version=1,
key=f"{DOMAIN}_userdata_{entry.entry_id}",
)
self.credit_limits: dict[str, float] = {}
self.aprs: dict[str, float] = {}
self.due_days: dict[str, int] = {}

# Get the user-defined update interval with fallbacks: options -> data -> default
update_interval = (
Expand All @@ -83,8 +93,87 @@ def __init__(self, hass: HomeAssistant, entry: ConfigEntry, budget_id: str, budg

async def async_load_persistent_data(self):
"""Load persistent data during coordinator initialization."""
await self._load_user_values()
await self._load_persistent_data()

async def _load_user_values(self) -> None:
"""Load persisted user-edited credit settings."""
stored = await self._user_store.async_load() or {}

self.credit_limits = {
key: float(value) for key, value in stored.get("credit_limits", {}).items()
}
self.aprs = {
key: float(value) for key, value in stored.get("aprs", {}).items()
}
self.due_days = {
key: int(value) for key, value in stored.get("due_days", {}).items()
}

# One-time migration from old config-entry options.
migrated = False
opts = self.entry.options
if not self.credit_limits and "credit_limits" in opts:
self.credit_limits = {
key: float(value) for key, value in opts["credit_limits"].items()
}
migrated = True

if not self.aprs and "aprs" in opts:
self.aprs = {
key: float(value) for key, value in opts["aprs"].items()
}
migrated = True

if not self.due_days and "due_days" in opts:
self.due_days = {
key: int(value) for key, value in opts["due_days"].items()
}
migrated = True

if migrated:
new_opts = dict(opts)
new_opts.pop("credit_limits", None)
new_opts.pop("aprs", None)
new_opts.pop("due_days", None)
self.hass.config_entries.async_update_entry(self.entry, options=new_opts)

await self.async_save_user_values()

async def async_save_user_values(self) -> None:
"""Persist user-edited credit settings."""
await self._user_store.async_save(
{
"credit_limits": self.credit_limits,
"aprs": self.aprs,
"due_days": self.due_days,
}
)

def get_credit_limit(self, account_id: str) -> float:
return float(self.credit_limits.get(account_id, 0.0))

def get_apr(self, account_id: str) -> float:
return float(self.aprs.get(account_id, 0.0))

def get_due_day(self, account_id: str) -> int | None:
return self.due_days.get(account_id)

async def async_set_credit_limit(self, account_id: str, value: float) -> None:
self.credit_limits[account_id] = float(value)
await self.async_save_user_values()
self.async_set_updated_data(self.data)

async def async_set_due_day(self, account_id: str, value: int) -> None:
self.due_days[account_id] = int(value)
await self.async_save_user_values()
self.async_set_updated_data(self.data)

async def async_set_apr(self, account_id: str, value: float) -> None:
self.aprs[account_id] = float(value)
await self.async_save_user_values()
self.async_set_updated_data(self.data)

def get_current_month(self):
"""Returns the current month in YYYY-MM-01 format."""
return datetime.now().strftime("%Y-%m-01")
Expand Down Expand Up @@ -185,6 +274,15 @@ async def _async_update_data(self):
budget_data["accounts"] = [
a for a in accounts.get("accounts", []) if a["id"] in self.selected_accounts
]

for account in budget_data["accounts"]:
account_id = account["id"]
if account_id in self.credit_limits:
account["credit_limit"] = self.credit_limits[account_id]
if account_id in self.aprs:
account["apr"] = self.aprs[account_id]
if account_id in self.due_days:
account["due_day"] = self.due_days[account_id]

_LOGGER.debug(f"🔹 Filtered Accounts: {budget_data['accounts']}")

Expand Down
136 changes: 136 additions & 0 deletions custom_components/ynab_custom/number.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"""Number entities for YNAB credit account settings."""

from homeassistant.components.number import NumberEntity
from homeassistant.const import EntityCategory
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import DOMAIN


class YNABCreditLimitNumber(CoordinatorEntity, NumberEntity):
"""Editable credit limit for credit/line of credit accounts."""

_attr_icon = "mdi:account-credit-card"
_attr_mode = "box"
_attr_native_min_value = 0
_attr_native_max_value = 20000
_attr_native_step = 1
_attr_entity_category = EntityCategory.CONFIG

def __init__(self, coordinator, account, entry):
super().__init__(coordinator)
self.coordinator = coordinator
self.account = account

account_id = account["id"]
budget_id = entry.data["budget_id"]

self._attr_name = f"{account['name']} Credit Limit"
self._attr_unique_id = f"{budget_id}_{account_id}_credit_limit"
self._attr_native_unit_of_measurement = coordinator.currency_symbol
self._attr_device_info = {
"identifiers": {(DOMAIN, f"{budget_id}_{account_id}")},
"name": account["name"],
"manufacturer": "YNAB",
"model": "Credit Card",
}

@property
def native_value(self):
return self.coordinator.get_credit_limit(self.account["id"])

async def async_set_native_value(self, value: float) -> None:
self.account["credit_limit"] = value
await self.coordinator.async_set_credit_limit(self.account["id"], value)
self.async_write_ha_state()


class YNABDueDayNumber(CoordinatorEntity, NumberEntity):
"""Monthly credit card payment due day (1-28)."""

_attr_icon = "mdi:calendar-month"
_attr_mode = "box"
_attr_native_min_value = 1
_attr_native_max_value = 28
_attr_native_step = 1
_attr_entity_category = EntityCategory.CONFIG

def __init__(self, coordinator, account, entry):
super().__init__(coordinator)
self.coordinator = coordinator
self.account = account

account_id = account["id"]
budget_id = entry.data["budget_id"]

self._attr_name = f"{account['name']} Due Day"
self._attr_unique_id = f"{budget_id}_{account_id}_due_day"
self._attr_device_info = {
"identifiers": {(DOMAIN, f"{budget_id}_{account_id}")},
"name": account["name"],
"manufacturer": "YNAB",
"model": "Credit Card",
}

@property
def native_value(self):
return self.coordinator.get_due_day(self.account["id"])

async def async_set_native_value(self, value: float) -> None:
await self.coordinator.async_set_due_day(self.account["id"], int(value))
self.async_write_ha_state()


class YNABAPRNumber(CoordinatorEntity, NumberEntity):
"""Editable APR for credit accounts."""

_attr_icon = "mdi:percent"
_attr_mode = "box"
_attr_native_unit_of_measurement = "%"
_attr_native_min_value = 0
_attr_native_max_value = 40
_attr_native_step = 0.01
_attr_entity_category = EntityCategory.CONFIG

def __init__(self, coordinator, account, entry):
super().__init__(coordinator)
self.coordinator = coordinator
self.account = account

account_id = account["id"]
budget_id = entry.data["budget_id"]

self._attr_name = f"{account['name']} APR"
self._attr_unique_id = f"{budget_id}_{account_id}_apr"
self._attr_device_info = {
"identifiers": {(DOMAIN, f"{budget_id}_{account_id}")},
"name": account["name"],
"manufacturer": "YNAB",
"model": "Credit Card",
}

@property
def native_value(self):
return self.coordinator.get_apr(self.account["id"])

async def async_set_native_value(self, value: float) -> None:
self.account["apr"] = value
await self.coordinator.async_set_apr(self.account["id"], value)
self.async_write_ha_state()


async def async_setup_entry(hass, entry, async_add_entities):
"""Set up YNAB number entities."""
coordinator = hass.data[DOMAIN][entry.entry_id]
entities = []

for account in coordinator.data.get("accounts", []):
account_type = account.get("type")
if account_type in ("creditCard", "lineOfCredit"):
entities.append(YNABCreditLimitNumber(coordinator, account, entry))
entities.append(YNABDueDayNumber(coordinator, account, entry))

if account_type in ("creditCard", "personalLoan"):
entities.append(YNABAPRNumber(coordinator, account, entry))

async_add_entities(entities)
Loading