Skip to content

Commit 7f74c56

Browse files
authored
Enhance PocketSmith integration with new uncategorised transactions sensor and improved data fetching
Refactored existing sensor: The PocketSmithSensor class now inherits from SensorEntity instead of Entity, allowing it to utilize sensor-specific properties and methods within Home Assistant. Introduced the Debouncer: Added Debouncer functionality to the PocketSmithSensor class to limit update frequency, ensuring that data fetching is optimized and reduces unnecessary API calls. Improved data fetching: Utilized Home Assistant’s async_get_clientsession for efficient session handling. Data fetching logic was modified to include only necessary attributes, reducing clutter and potential API overhead. New unique ID format: Adjusted the unique ID structure to incorporate both account ID and title, ensuring truly unique identifiers and preventing conflicts. Added a new sensor for uncategorized transactions: Introduced the PocketsmithUncategorisedTransactions class, which provides a dedicated sensor for tracking the count of uncategorized transactions associated with the user’s PocketSmith account. Enhanced logging and error handling: Improved error handling across both sensors with detailed logging for better diagnostics and debugging. Icon customization: Added specific Material Design icons (mdi:currency-usd and mdi:alert-circle-outline) to enhance the visual representation of the sensors in the Home Assistant UI. Reduced data fetching intervals: Implemented cooldown periods of 60 seconds for account balance updates and 5 minutes for uncategorized transactions, minimizing API load and improving performance.
1 parent 04d8bd9 commit 7f74c56

File tree

1 file changed

+185
-58
lines changed

1 file changed

+185
-58
lines changed

custom component files/sensor.py

Lines changed: 185 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,121 +1,248 @@
1-
# PocketSmith Sensor.
2-
from homeassistant.helpers.entity import Entity
3-
import aiohttp
41
import logging
5-
from .const import DOMAIN
2+
import aiohttp
3+
from homeassistant.components.sensor import SensorEntity # Use SensorEntity for sensor-specific properties
4+
from homeassistant.helpers.aiohttp_client import async_get_clientsession # Use Home Assistant's session manager
5+
from homeassistant.helpers.debounce import Debouncer # Import Debouncer for throttling updates
6+
from .const import DOMAIN # Import domain constant
67

78
_LOGGER = logging.getLogger(__name__)
89

910
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
10-
# Set up PocketSmith sensors.
11+
"""Set up PocketSmith sensors (existing and new)."""
1112
developer_key = hass.data[DOMAIN]["developer_key"]
13+
1214
try:
13-
user_id = await get_user_id(developer_key)
14-
user_accounts = await get_user_accounts(developer_key, user_id)
15-
async_add_entities([PocketSmithSensor(developer_key, account) for account in user_accounts])
15+
# Retrieve user ID and accounts from PocketSmith
16+
user_id = await get_user_id(hass, developer_key)
17+
user_accounts = await get_user_accounts(hass, developer_key, user_id)
18+
19+
# Create PocketSmith sensors for each account
20+
sensors = [PocketSmithSensor(hass, developer_key, account) for account in user_accounts]
21+
22+
# Add a sensor for uncategorised transactions
23+
sensors.append(PocketsmithUncategorisedTransactions(hass, developer_key, user_id))
24+
25+
async_add_entities(sensors)
1626
except Exception as e:
1727
_LOGGER.error(f"Error setting up PocketSmith platform: {e}")
1828

19-
class PocketSmithSensor(Entity):
20-
# Representation of a PocketSmith Sensor.
29+
class PocketSmithSensor(SensorEntity):
30+
"""Representation of a PocketSmith Account Balance Sensor."""
2131

22-
def __init__(self, developer_key, account):
23-
# Initialize the sensor.
32+
def __init__(self, hass, developer_key, account):
33+
"""Initialize the sensor."""
34+
self._hass = hass
2435
self._developer_key = developer_key
2536
self._account = account
2637
self._state = None
2738
self._attributes = {}
39+
self._debouncer = None # For managing throttling updates
2840

2941
@property
3042
def unique_id(self):
31-
# Return a unique ID for the sensor.
32-
return f"pocketsmith_{self._account['id']}"
43+
"""Return a truly unique ID for the sensor using the account ID and title."""
44+
# Ensure the title is converted to a format that is unique and matches your existing entity name convention
45+
account_title = self._account.get('title', 'Unnamed Account').replace(" ", "_").lower()
46+
return f"pocketsmith_{self._account['id']}_{account_title}_balance"
3347

3448
@property
3549
def name(self):
36-
# Return the name of the sensor.
37-
return f"PocketSmith Account {self._account.get('title', 'Unnamed Account')}"
50+
"""Return the name of the sensor."""
51+
return f"PocketSmith Account {self._account.get('title', 'Unnamed Account')} Balance"
3852

3953
@property
4054
def state(self):
41-
# Return the state of the sensor.
55+
"""Return the current balance as the state of the sensor."""
4256
return self._state
4357

4458
@property
4559
def unit_of_measurement(self):
46-
# Return the unit of measurement.
60+
"""Return the unit of measurement (currency)."""
4761
return self._account.get('currency_code', 'USD').upper()
4862

4963
@property
5064
def device_class(self):
51-
# Return the device class.
65+
"""Return the device class for this sensor."""
5266
return "monetary"
5367

68+
@property
69+
def icon(self):
70+
"""Return an icon representing the sensor."""
71+
return "mdi:currency-usd"
72+
5473
@property
5574
def extra_state_attributes(self):
56-
# Return the state attributes.
75+
"""Return additional state attributes."""
5776
return self._attributes
5877

78+
async def async_added_to_hass(self):
79+
"""Initialize the debouncer when added to Home Assistant."""
80+
if not self._debouncer:
81+
self._debouncer = Debouncer(
82+
hass=self._hass,
83+
logger=_LOGGER,
84+
cooldown=60, # Update no more than once every 60 seconds
85+
immediate=True,
86+
function=self.async_update_data
87+
)
88+
5989
async def async_update(self):
60-
# Fetch new state data for the sensor.
90+
"""Throttle the update call using the Debouncer."""
91+
if self._debouncer:
92+
await self._debouncer.async_call()
93+
94+
async def async_update_data(self):
95+
"""Fetch the latest data from the PocketSmith API."""
6196
try:
6297
self._state = await self.fetch_data()
6398
except Exception as e:
6499
_LOGGER.error(f"Error updating PocketSmith sensor: {e}")
65100

66101
async def fetch_data(self):
67-
# Fetch data from PocketSmith API.
102+
"""Fetch account balance data from PocketSmith API."""
68103
headers = {
69104
"Accept": "application/json",
70105
"Authorization": f"Key {self._developer_key}"
71106
}
72107
url = f"https://api.pocketsmith.com/v2/accounts/{self._account['id']}"
73-
74-
async with aiohttp.ClientSession() as session:
75-
async with session.get(url, headers=headers) as response:
76-
if response.status == 200:
77-
data = await response.json()
78-
_LOGGER.debug(f"Fetched data for account {self._account['id']}: {data}")
79-
balance = data.get("current_balance")
80-
if balance is None:
81-
_LOGGER.error(f"No 'current_balance' field in response for account {self._account['id']}: {data}")
82-
# Update the state attributes with all the information from the response
83-
self._attributes = data
84-
return balance
85-
else:
86-
_LOGGER.error(f"Failed to fetch data for account {self._account['id']}. Status code: {response.status}")
87-
return None
88-
89-
async def get_user_id(developer_key):
90-
# Retrieve the user ID using the developer key.
108+
109+
session = async_get_clientsession(self._hass)
110+
async with session.get(url, headers=headers) as response:
111+
if response.status == 200:
112+
data = await response.json()
113+
_LOGGER.debug(f"Fetched data for account {self._account['id']}: {data}")
114+
115+
# Set the balance as the state
116+
balance = data.get("current_balance", 0.0) # Default to 0.0 if missing
117+
118+
# Extract only the necessary fields
119+
transaction_accounts = data.get("transaction_accounts", [])
120+
filtered_accounts = []
121+
for account in transaction_accounts:
122+
filtered_account = {
123+
"id": account.get("id"),
124+
"account_id": account.get("account_id"),
125+
"name": account.get("name"),
126+
"current_balance": account.get("current_balance")
127+
}
128+
filtered_accounts.append(filtered_account)
129+
130+
# Set only the filtered attributes
131+
self._attributes = {"transaction_accounts": filtered_accounts}
132+
133+
return balance
134+
else:
135+
_LOGGER.error(f"Failed to fetch data for account {self._account['id']}. Status code: {response.status}")
136+
return None
137+
138+
class PocketsmithUncategorisedTransactions(SensorEntity):
139+
"""Representation of a PocketSmith Sensor for counting uncategorised transactions."""
140+
141+
def __init__(self, hass, developer_key, user_id):
142+
"""Initialize the sensor."""
143+
self._hass = hass
144+
self._developer_key = developer_key
145+
self._user_id = user_id
146+
self._state = None
147+
self._debouncer = None
148+
149+
@property
150+
def unique_id(self):
151+
"""Return a unique ID for the sensor."""
152+
return f"pocketsmith_{self._user_id}_uncategorised_transactions"
153+
154+
@property
155+
def name(self):
156+
"""Return the name of the sensor."""
157+
return "Pocketsmith Uncategorised Transactions"
158+
159+
@property
160+
def state(self):
161+
"""Return the count of uncategorized transactions."""
162+
return self._state
163+
164+
@property
165+
def unit_of_measurement(self):
166+
"""Return the unit of measurement (transactions count)."""
167+
return "transactions"
168+
169+
@property
170+
def icon(self):
171+
"""Return an appropriate icon for the sensor."""
172+
return "mdi:alert-circle-outline"
173+
174+
async def async_added_to_hass(self):
175+
"""Initialize the debouncer when added to Home Assistant."""
176+
if not self._debouncer:
177+
self._debouncer = Debouncer(
178+
hass=self._hass,
179+
logger=_LOGGER,
180+
cooldown=300, # Update no more than once every 5 minutes
181+
immediate=True,
182+
function=self.async_update_data
183+
)
184+
185+
async def async_update(self):
186+
"""Throttle the update call using the Debouncer."""
187+
if self._debouncer:
188+
await self._debouncer.async_call()
189+
190+
async def async_update_data(self):
191+
"""Fetch uncategorised transactions count from the PocketSmith API."""
192+
try:
193+
self._state = await self.fetch_uncategorised_transactions_count()
194+
except Exception as e:
195+
_LOGGER.error(f"Error updating Pocketsmith uncategorised transactions sensor: {e}")
196+
197+
async def fetch_uncategorised_transactions_count(self):
198+
"""Fetch transactions and count those with a null category."""
199+
headers = {
200+
"Accept": "application/json",
201+
"Authorization": f"Key {self._developer_key}"
202+
}
203+
url = f"https://api.pocketsmith.com/v2/users/{self._user_id}/transactions"
204+
205+
session = async_get_clientsession(self._hass)
206+
async with session.get(url, headers=headers) as response:
207+
if response.status == 200:
208+
transactions = await response.json()
209+
null_category_count = sum(1 for transaction in transactions if transaction.get("category") is None)
210+
_LOGGER.debug(f"Number of uncategorised transactions: {null_category_count}")
211+
return null_category_count
212+
else:
213+
_LOGGER.error(f"Failed to fetch transactions for user {self._user_id}. Status code: {response.status}")
214+
return None
215+
216+
async def get_user_id(hass, developer_key):
217+
"""Retrieve the user ID using the developer key."""
91218
url = "https://api.pocketsmith.com/v2/me"
92219
headers = {
93220
"Accept": "application/json",
94221
"Authorization": f"Key {developer_key}"
95222
}
96223

97-
async with aiohttp.ClientSession() as session:
98-
async with session.get(url, headers=headers) as response:
99-
if response.status == 200:
100-
data = await response.json()
101-
return data.get("id")
102-
else:
103-
_LOGGER.error(f"Failed to retrieve user ID. Status code: {response.status}")
104-
response.raise_for_status()
224+
session = async_get_clientsession(hass)
225+
async with session.get(url, headers=headers) as response:
226+
if response.status == 200:
227+
data = await response.json()
228+
return data.get("id")
229+
else:
230+
_LOGGER.error(f"Failed to retrieve user ID. Status code: {response.status}")
231+
response.raise_for_status()
105232

106-
async def get_user_accounts(developer_key, user_id):
107-
# Retrieve the user's accounts using the user ID.
233+
async def get_user_accounts(hass, developer_key, user_id):
234+
"""Retrieve the user's accounts using the user ID."""
108235
url = f"https://api.pocketsmith.com/v2/users/{user_id}/accounts"
109236
headers = {
110237
"Accept": "application/json",
111238
"Authorization": f"Key {developer_key}"
112239
}
113240

114-
async with aiohttp.ClientSession() as session:
115-
async with session.get(url, headers=headers) as response:
116-
if response.status == 200:
117-
data = await response.json()
118-
return data
119-
else:
120-
_LOGGER.error(f"Failed to retrieve user accounts. Status code: {response.status}")
121-
response.raise_for_status()
241+
session = async_get_clientsession(hass)
242+
async with session.get(url, headers=headers) as response:
243+
if response.status == 200:
244+
data = await response.json()
245+
return data
246+
else:
247+
_LOGGER.error(f"Failed to retrieve user accounts. Status code: {response.status}")
248+
response.raise_for_status()

0 commit comments

Comments
 (0)