Skip to content

Instantly share code, notes, and snippets.

@psybers
Created June 16, 2024 21:17
Show Gist options
  • Save psybers/56a7947282c734cec5b68253a5a7ceb6 to your computer and use it in GitHub Desktop.
Save psybers/56a7947282c734cec5b68253a5a7ceb6 to your computer and use it in GitHub Desktop.
Have Actual Budget track a home's zestimate. Add "zestimate:https://www.zillow.com/homedetails/HOME/ID_zpid/" to the account note. If you own a percent of the house, add "ownership:0.50" to the note. Run `npm install` to install packages, then `node zestimate.js` to run it (I run it in a weekly/monthly cron).
{
"dependencies": {
"@actual-app/api": "^6.8.1",
"dotenv": "^16.4.5",
"jsdom": "^24.1.0"
}
}
require("dotenv").config();
const api = require('@actual-app/api');
const payeeName = process.env.IMPORTER_PAYEE_NAME || 'Zestimate';
const url = process.env.ACTUAL_SERVER_URL || '';
const password = process.env.ACTUAL_SERVER_PASSWORD || '';
const file_password = process.env.ACTUAL_FILE_PASSWORD || '';
const sync_id = process.env.ACTUAL_SYNC_ID || '';
const cache = process.env.IMPORTER_CACHE_DIR || './cache';
if (!url || !password || !sync_id) {
console.error('Required settings for Actual not provided.');
process.exit(1);
}
const jsdom = require("jsdom");
async function getZestimate(URL) {
const response = await fetch(URL, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
'Accept-Language': 'en-GB,en;q=0.6',
'Referer': 'https://www.google.com/',
}
});
const html = await response.text();
const dom = new jsdom.JSDOM(html);
const zestimateText = dom.window.document.getElementById('home-details-home-values').getElementsByTagName('h3')[0].textContent;
return parseInt(zestimateText.replace('$', '').replace(',', '')) * 100;
}
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
const getAccountBalance = async (account) => {
const data = await api.runQuery(
api.q('transactions')
.filter({
'account': account.id,
})
.calculate({ $sum: '$amount' })
.options({ splits: 'grouped' })
);
return data.data;
};
(async function() {
console.log("connect");
await api.init({ serverURL: url, password: password, dataDir: cache });
console.log("open file");
if (file_password) {
await api.downloadBudget(sync_id, { password: file_password, });
} else {
await api.downloadBudget(sync_id);
}
const payees = await api.getPayees();
let payeeId = payees.find(p => p.name === payeeName).id;
if (!payeeId) {
payeeId = await api.createPayee({ name: payeeName });
}
if (!payeeId) {
console.error('Failed to create payee:', payeeName);
process.exit(1);
}
const accounts = await api.getAccounts();
for (const account of accounts) {
const notes = await api.runQuery(
api.q('notes')
.filter({
id: `account-${account.id}`,
})
.select('*')
);
if (notes.data.length && notes.data[0].note) {
const note = notes.data[0].note;
if (note.indexOf('zestimate:') > -1) {
const URL = note.split('zestimate:')[1].split(' ')[0];
let ownership = 1;
if (note.indexOf('ownership:') > -1) {
ownership = parseFloat(note.split('ownership:')[1].split(' ')[0]);
}
console.log('Fetching zestimate for account:', account.name);
console.log('Zillow URL:', URL);
const zestimate = await getZestimate(URL);
const balance = await getAccountBalance(account);
const diff = (zestimate * ownership) - balance;
console.log('Zestimate:', zestimate);
console.log('Ownership:', zestimate * ownership);
console.log('Balance:', balance);
console.log('Difference:', diff);
if (diff != 0) {
await api.importTransactions(account.id, [{
date: new Date(),
payee: payeeId,
amount: diff,
notes: `Update Zestimate to ${zestimate * ownership / 100} (${zestimate / 100}*${ownership * 100}%)`,
}]);
}
await sleep(1324);
}
}
}
console.log("done");
await api.shutdown();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment