Compare commits

...

15 Commits

Author SHA1 Message Date
chris f50fff0df9 up reqs 2022-07-04 14:54:49 +02:00
chris 9aa3422be9 up reqs 2022-07-04 14:47:10 +02:00
chris 5350fef36d fix transaction query 2021-09-21 14:19:24 +02:00
chris e800282667 TZ aware 2021-09-20 17:44:29 +02:00
chris eee96d14b3 line breaks 2020-11-05 16:37:01 +01:00
chris f1bc9408db fix 2020-11-05 13:59:28 +01:00
chris d13ef5dfd8 report new average data 2020-11-05 13:21:53 +01:00
chris 715b0a2fbf add more averages 2020-11-05 13:20:00 +01:00
chris f2a5ec3990 first bot draft 2020-11-02 00:39:25 +01:00
chris 340ff5270e move helpers 2020-11-01 16:53:51 +01:00
chris 4a1eccc3a2 aioxmpp 2020-11-01 16:53:51 +01:00
chris bbfffb383d self-contain reqs 2020-11-01 16:53:51 +01:00
chris 71781ae3be xmpp script 2020-11-01 16:53:51 +01:00
chris daf5eb07ce draw budget summary 2020-11-01 16:53:51 +01:00
chris 91c5ec3f79 query budget data 2020-11-01 16:53:51 +01:00
9 changed files with 305 additions and 9 deletions

View File

@ -32,3 +32,47 @@ Or use the prebuilt docker image:
docker run -e FIREFLY_API_HOST="https://your-firefly-installation" -e FIREFLY_PERSONAL_ACCESS_TOKEN="abcd....1234" -p 9449:5000 zknt/firefly-exporter
and scrape port 9449 on your docker host.
## XMPP Bot
There is a simple XMPP / Jabber bot which responds to budget queries.
### Installation
Install requirements:
pip install -r requirements-xmppbot.txt
Use your personal access token from above, setting the following variables
to your environment:
export FIREFLY_JABBER_ID="firefly@example.com" # The bots JID
export FIREFLY_JABBER_PASSWORD="abcd...1234" # The bots xmpp password
export FIREFLY_API_HOST="https://yout-firefly-installation"
export FIREFLY_PERSONAL_ACCESS_TOKEN="1234...abcd"
### Preparation
Register a XMPP / Jabber account for your bot.
*Caveat*: The bot does not support E2E and handles your financial data, so make
sure the account resides on the same server as your personal account which you will
use for querying.
Log into your bot account with a XMPP client and add your personal JID to your roster.
Querying will only work with a two-way subscription.
## Bot usage
Run `python xmppbot.py`.
From your personal account, send a normal chat message to your bot.
Available commands:
* `budgets` - list all budgets names
* `budget <budget>` - query data for given budget
### Docker
Run the prebuilt image:
docker run -e FIREFLY_JABBER_ID="firefly@example.com" -e FIREFLY_JABBER_PASSWORD="abcd...1234" -e FIREFLY_API_HOST="https://yout-firefly-installation" -e FIREFLY_PERSONAL_ACCESS_TOKEN="1234...abcd" zknt/firefly-xmppbot

39
budgetmail.py Normal file
View File

@ -0,0 +1,39 @@
import calendar
import datetime
import firefly.budgets
def _get_remaining_days():
today = datetime.datetime.combine(datetime.date.today(), datetime.datetime.min.time())
endofmonth = today.replace(day=calendar.monthrange(today.year, today.month)[1])
return (endofmonth - today).days + 1
def _collect_budget_data():
budgets = []
for budget in firefly.budgets._get_budgets():
budgets.append({
"id": budget.get('id'),
"name": budget.get('attributes').get('name'),
"limit": firefly.budgets._get_current_limit(budget.get('id')).get('attributes').get('amount'),
"spent": firefly.budgets._get_current_spent_amount(budget.get('id')),
})
currbudget = budgets[-1]
currbudget['remaining'] = (float(currbudget.get('limit')) - float(currbudget.get('spent')))
remaining_per_day = currbudget.get('remaining') / _get_remaining_days()
currbudget['remaining_per_day'] = 0 if remaining_per_day < 0 else remaining_per_day
return budgets
def main():
for budget in _collect_budget_data():
print("Budget: " + budget.get('name'))
print(" Limit: " + budget.get('limit'))
print(" Spent: {:.2f}".format(budget.get('spent')))
print(" Remaining: {:.2f}".format(budget.get('remaining')))
print(" Remaining per day: {:.2f}".format(budget.get('remaining_per_day')))
if __name__ == "__main__":
main()

59
budgetxmpp.py Normal file
View File

@ -0,0 +1,59 @@
import calendar
import datetime
import xmpp
import time
import os
import firefly.budgets
def _get_remaining_days():
today = datetime.datetime.combine(datetime.date.today(), datetime.datetime.min.time())
endofmonth = today.replace(day=calendar.monthrange(today.year, today.month)[1])
return (endofmonth - today).days + 1
def _collect_budget_data():
budgets = []
for budget in firefly.budgets._get_budgets():
budgets.append({
"id": budget.get('id'),
"name": budget.get('attributes').get('name'),
"limit": firefly.budgets._get_current_limit(budget.get('id')).get('attributes').get('amount'),
"spent": firefly.budgets._get_current_spent_amount(budget.get('id')),
})
currbudget = budgets[-1]
currbudget['remaining'] = (float(currbudget.get('limit')) - float(currbudget.get('spent')))
remaining_per_day = currbudget.get('remaining') / _get_remaining_days()
currbudget['remaining_per_day'] = 0 if remaining_per_day < 0 else remaining_per_day
return budgets
def main():
jid = xmpp.protocol.JID(os.environ.get("FIREFLY_JABBER_ID"))
connection = xmpp.Client(server=jid.getDomain())
connection.connect()
connection.auth(
user=jid.getNode(),
password=os.environ.get("FIREFLY_JABBER_PASSWORD"),
resource=jid.getResource(),
)
for budget in _collect_budget_data():
message = "Budget: " + budget.get('name')
message += "\n Limit: " + budget.get('limit')
message += "\n Spent: {:.2f}".format(budget.get('spent'))
message += "\n Remaining: {:.2f}".format(budget.get('remaining'))
message += "\n Remaining per day: {:.2f}".format(budget.get('remaining_per_day'))
connection.send(
xmpp.protocol.Message(
to=os.environ.get('FIREFLY_JABBER_RECEIVER'),
body=message,
),
)
time.sleep(1)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,14 @@
"""Firefly3 API modules"""
import datetime
import calendar
def get_remaining_days():
"""Get number of days remaining in current month"""
today = datetime.datetime.combine(
datetime.date.today(), datetime.datetime.min.time()
)
endofmonth = today.replace(
day=calendar.monthrange(today.year, today.month)[1]
)
return (endofmonth - today).days + 1

View File

@ -3,18 +3,22 @@ import os
import calendar
import datetime
import pytz
import requests
import dateutil.parser
import firefly
header = {"Authorization": "Bearer " + os.environ.get('FIREFLY_PERSONAL_ACCESS_TOKEN')}
host = os.environ.get('FIREFLY_API_HOST')
def _get_budgets():
budgets_json = requests.get(
budgets_req = requests.get(
host + "/api/v1/budgets", headers=header
).json()
)
budgets_json = budgets_req.json()
budgets = budgets_json.get('data')
while "next" in budgets_json.get('links'):
budgets_json = requests.get(
@ -35,7 +39,7 @@ def _get_current_limit(budget_id):
).json()
budgets.extend(budget_json.get('data'))
today = datetime.datetime.combine(datetime.date.today(), datetime.datetime.min.time())
today = datetime.datetime.combine(datetime.date.today(), datetime.datetime.min.time()).replace(tzinfo=pytz.UTC)
return [
budget for budget in budgets
if today >= dateutil.parser.parse(budget.get('attributes').get('start')) and today <= dateutil.parser.parse(budget.get('attributes').get('end'))
@ -44,11 +48,13 @@ def _get_current_limit(budget_id):
def _get_current_transactions(budget_id):
limit_id = _get_current_limit(budget_id).get('id')
transactions_json = requests.get(
host + "/api/v1/budgets/limits/{}/transactions".format(
transactions_req = requests.get(
host + "/api/v1/budgets/{}/limits/{}/transactions".format(
budget_id,
limit_id
), headers=header
).json()
)
transactions_json = transactions_req.json()
transactions = transactions_json.get('data')
while 'next' in transactions_json:
transactions_json = requests.get(
@ -70,6 +76,24 @@ def _get_current_spent_amount(budget_id):
return spent_amount
def _collect_budget_data():
budgets = []
for budget in _get_budgets():
budgets.append({
"id": budget.get('id'),
"name": budget.get('attributes').get('name'),
"limit": float(_get_current_limit(budget.get('id')).get('attributes').get('amount')),
"spent": _get_current_spent_amount(budget.get('id')),
})
currbudget = budgets[-1]
currbudget['remaining'] = (float(currbudget.get('limit')) - float(currbudget.get('spent')))
remaining_per_day = currbudget.get('remaining') / firefly.get_remaining_days()
currbudget['remaining_per_day'] = 0 if remaining_per_day < 0 else remaining_per_day
currbudget['spent_per_day'] = currbudget['spent'] / datetime.datetime.today().day
currbudget['average_per_day'] = currbudget['limit'] / (datetime.datetime.today().day + firefly.get_remaining_days() - 1)
return budgets
def get_budget_metrics():
out = ""
for budget in _get_budgets():

3
requirements-xmpp.txt Normal file
View File

@ -0,0 +1,3 @@
python-dateutil==2.8.1
requests==2.24.0
xmpppy==0.6.1

3
requirements-xmppbot.txt Normal file
View File

@ -0,0 +1,3 @@
aioxmpp==0.11.0
python-dateutil==2.8.2
requests==2.28.1

View File

@ -1,3 +1,4 @@
Flask==1.1.2
python-dateutil==2.8.1
requests==2.24.0
Flask==2.1.2
python-dateutil==2.8.2
requests==2.28.1
pytz==2022.1

109
xmppbot.py Normal file
View File

@ -0,0 +1,109 @@
import os
import signal
import asyncio
import aioxmpp
import firefly.budgets
class BudgetBot(object):
def message_received(self, msg):
if msg.type_ != aioxmpp.MessageType.CHAT:
return
if not msg.body:
return
if str(msg.from_).split('/')[0] not in self.controller:
print("Ignored message from: {}".format(msg.from_))
return
print("msg from: {}".format(msg.from_))
print("got: {}".format(list(msg.body.values())[0]))
budget_data = firefly.budgets._collect_budget_data()
command = list(msg.body.values())[0]
if command.lower() == "budgets":
reply = "Budgets:\n"
for budget in budget_data:
reply += "- *{}*\n".format(budget.get('name'))
elif command.lower().startswith("budget "):
cmd_budget = command.split(' ')[1].lower()
budget = [b for b in budget_data if b['name'].lower() == cmd_budget]
if len(budget) != 1:
reply = "Invalid budget name"
else:
reply = "Budget *{}*\n".format(budget[0]['name'])
reply += " Available current month: {:.2f}\n".format(budget[0].get('limit'))
reply += " Average per day: {:.2f}\n".format(budget[0].get('average_per_day'))
reply += " Spent: {:.2f}\n".format(budget[0].get('spent'))
reply += " Spent per day: {:.2f}\n".format(budget[0].get('spent_per_day'))
reply += " Remaining: {:.2f}\n".format(budget[0].get('remaining'))
reply += " Remaining per day: {:.2f}".format(budget[0].get('remaining_per_day'))
else:
reply = "Please send command:\n"
reply += "- *budgets* list all budgets"
reply_msg = aioxmpp.Message(
type_=msg.type_,
to=msg.from_,
)
reply_msg.body[None] = reply
self.client.enqueue(reply_msg)
async def run_simple_example(self):
stop_event = self.make_sigint_event()
self.client.stream.register_message_callback(
aioxmpp.MessageType.CHAT,
None,
self.message_received,
)
print("listening... (press Ctrl-C or send SIGTERM to stop)")
await stop_event.wait()
async def run_example(self):
self.client = self.make_simple_client()
async with self.client.connected():
await self.run_simple_example()
def make_simple_client(self):
client = aioxmpp.PresenceManagedClient(
aioxmpp.JID.fromstr(os.environ.get('FIREFLY_JABBER_ID')),
aioxmpp.make_security_layer(os.environ.get('FIREFLY_JABBER_PASSWORD')),
)
self.roster = client.summon(aioxmpp.RosterClient)
self.roster.on_initial_roster_received.connect(
self._read_roster,
)
return client
def _read_roster(self):
self.controller = []
for item in self.roster.items.values():
self.controller.append(str(item.jid))
print("Roster entry: {}".format(item.jid))
print("Subscription: {}".format(item.subscription))
print("Approved: {}".format(item.approved))
print("Allowed controllers: {}".format(self.controller))
def make_sigint_event(self):
event = asyncio.Event()
loop = asyncio.get_event_loop()
loop.add_signal_handler(
signal.SIGINT,
event.set,
)
return event
def main():
bot = BudgetBot()
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(bot.run_example())
finally:
loop.close()
if __name__ == "__main__":
main()