09-Oct-2024

GnuCash price updates

I've used GnuCash for a long time to track my finances. As I have a few shares and other investments, I previously used a cron job on my main Linux desktop machine to pull in pricing data about these each day. As the Australian Stock Exchange finishes trading at 4pm, and I'd usually be working on my computer until 5pm, this wasn't an issue. The machine would be powered on, and the cron job would execute in the background.

Things changed when I retired.

As the machine could be suspended or powered off if I was out doing something prior to 4pm, I was looking for a way to deal with the fact that cron is incapable of running a missed job without intervention.

I looked at anacron, but as I required the job to run once a day after the market was closed, this didn't really fit the bill.

Then I stumbled across an article explaining how to replace cron jobs with timers in systemd. And the Persistant=true flag.

The first new thing I learned was that you can make use of systemd services and timers just as a run-of-the-mill user. This should have been obvious, but for the fact that most discussions about systemd are how to use it to do system startup. Please note that the linked article is from the ArchLinux wiki and has some distro specific stuff in it. All the commands and systemd syntax are the same across distros however.

After some trial and error, I ended up with a timer and a service file. For Ubuntu Linux, these live in ~/.config/systemd/user

This is the timer file. I called it schedGnucash.timer


[Unit]
Description=Gets prices for GnuCash

[Timer]
Unit=schedGnucash.service
OnCalendar=Mon..Fri *-*-* 16:15:00
Persistent=true

[Install]
WantedBy=default.target

This says "Run a service called schedGnucash.service at 4:15pm, Monday to Friday". The Persistent=true flag says "If the machine is suspended or powered off at the scheduled time, run the service as soon as the machine is resumed/booted". Magic! Just what I wanted.

This is the schedGnucash.service file:


[Unit]
Description=Fetches prices for GnuCash

[Service]
Type=oneshot
ExecStart=/bin/bash $HOME/script/gnucash.sh
ExecStartPre=/bin/sh -c 'until host example.com; do sleep 1; done'

[Install]
WantedBy=graphical-session.target

This says "Start a bash script called gnucash.sh, once, and oh by the way, before you start it? Make sure DNS names resolve".

Because the dbus message service is used for notify-send and wmctrl commands (discussed below), we need to wait for the "desktop" to be be up, which is why the WantedBy target is what it is.

Next, we have the gnucash.sh script, run by the service:


#!/bin/bash
ph=$(python $HOME/script/pubhol.py)
if [ "$ph" == "False" ]; then
    if [ -f $HOME/Documents/GnuCash/'Retired Master GnuCash.gnucash.LCK' ]; then
        notify-send -t 120000 -A "GnuCash" "Retrieving prices for GnuCash in 2 minutes - please exit GnuCash"
        wmctrl -c 'Retired Master GnuCash.gnucash'
    fi
    gnucash-cli --quotes get $HOME/Documents/GnuCash/'Retired Master GnuCash.gnucash'
else
    echo "Public holiday"
fi

This says "If it's not a public holiday, check if Jim stupidly left GnuCash open based on the lock file, and if so, use the the desktop notify service to send him a warning with a 2 minute timeout that the price update is about to happen, then use the window manager control program to close a window whose name matches the name of the GnuCash database file. Then run the price update".

There are a couple of things to note here. One is that I have GnuCash set to auto-save data every couple of minutes, so if wmctrl closes the window, I don't lose any data. If I close GnuCash after seeing the notification, wmctrl attempts to close a window that is already closed, and just succeeds.

Lastly, we have a little Python script to determine if today is a public holiday and do nothing as there will have been no trading. This is very specific to NSW and Australia. If you have investments anywhere other than the Australian Stock Exchange, this won't work for you :)


"""
Based on the gov api documented here: 
https://data.gov.au/dataset/ds-dga-b1bc6077-dadd-4f61-9f8c-002ab2cdff10/details
this script returns True if today is a public holiday in NSW, else it returns False.
"""

import urllib.request
import json
from datetime import datetime

today = datetime.today ()
ymd = today.strftime ("%Y%m%d")

url = "https://data.gov.au/data/api/3/action/datastore_search_sql?"
sql = "SELECT * FROM \"33673aca-0857-42e5-b8f0-9981b4755686\" WHERE \"Date\"='%s' AND \"Jurisdiction\"='nsw'" % ymd 
params = {'sql' : sql}

url = url + urllib.parse.urlencode (params, quote_via=urllib.parse.quote)
fileobj = urllib.request.urlopen (url)
data = json.loads (fileobj.read ())
result = data['result']
records = result['records']
result = False
if (len (records)):
    if int (records[0]['ID']) > 0:
        result = True
print (result)

Note there is no hash bang on this script as I have multiple Python installs via Conda (don't ask :)

Posted at October 9, 2024 7:05 AM
Post a comment









Remember personal info?