Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 850c0d7a6a | |||
| 30be20eac6 | |||
| 6ed45c4785 | |||
| 77b7333e81 | |||
| 7899b45e9f | |||
| 526cb5fa3a | |||
| f967435881 | |||
| 867694ce9a | |||
| c653f8c28b | |||
| f02b397fbb | |||
| 72ba2dcb4f | |||
| db12151cc3 | |||
| 511a5a537f | |||
| aa02e0ef5b | |||
| 44eb9645b4 | |||
| a5c9c0fd6f | |||
| f18c6d1e1c | |||
| f3b115603d | |||
| 6ebcc1cc7a | |||
| a4c85a76d5 | |||
| 543bb71056 | |||
| eb46d2cc72 | |||
| d878f77fc0 | |||
| 4a24330dea | |||
| 2c6e37ffc6 | |||
| b2a45903f9 | |||
| c031e5bffd | |||
| 066b217347 | |||
| 95d4c80b62 | |||
| 18e58a5966 | |||
| 84ea8d2e76 | |||
| 9d82f204f9 | |||
| db2b47b180 | |||
| f56b811d64 | |||
| 1217bce02d | |||
| 0e376c02f4 | |||
| 0ce92abd16 | |||
| 6b6df500ee | |||
| 4dc30e44ff | |||
| 3792d96ca0 |
@@ -1,2 +1,7 @@
|
||||
.git/
|
||||
.idea/
|
||||
.vscode/
|
||||
.github/
|
||||
.gitignore
|
||||
node_modules/
|
||||
package-lock.json
|
||||
|
||||
@@ -43,6 +43,12 @@ jobs:
|
||||
# set latest tag for default branch
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and push the Docker image
|
||||
id: push
|
||||
uses: docker/build-push-action@v6
|
||||
@@ -52,3 +58,4 @@ jobs:
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
+12
-2
@@ -49,9 +49,9 @@ ENV ACTUAL_CACHE_DIR="./cache"
|
||||
# optional, name of the payee for added interest transactions
|
||||
ENV INTEREST_PAYEE_NAME="Loan Interest"
|
||||
|
||||
# optional, name of the payee for added interest transactions
|
||||
# optional, name of the payee for added investment transactions
|
||||
ENV INVESTMENT_PAYEE_NAME="Investment"
|
||||
# optional, name of the cateogry group for added investment tracking transactions
|
||||
# optional, name of the category group for added investment tracking transactions
|
||||
ENV INVESTMENT_CATEGORY_GROUP_NAME="Income"
|
||||
# optional, name of the category for added investment tracking transactions
|
||||
ENV INVESTMENT_CATEGORY_NAME="Investment"
|
||||
@@ -59,11 +59,21 @@ ENV INVESTMENT_CATEGORY_NAME="Investment"
|
||||
# optional, for logging into SimpleFIN
|
||||
ENV SIMPLEFIN_CREDENTIALS=""
|
||||
|
||||
# optional, name of the payee for Zestimate entries
|
||||
ENV ZESTIMATE_PAYEE_NAME="Zestimate"
|
||||
|
||||
# optional, name of the payee for KBB entries
|
||||
ENV KBB_PAYEE_NAME="KBB"
|
||||
|
||||
# optional, for retrieving Bitcoin Price (these default to Kraken USD)
|
||||
ENV BITCOIN_PRICE_URL="https://api.kraken.com/0/public/Ticker?pair=xbtusd"
|
||||
ENV BITCOIN_PRICE_JSON_PATH="result.XXBTZUSD.c[0]"
|
||||
ENV BITCOIN_PAYEE_NAME="Bitcoin Price Change"
|
||||
|
||||
#optional, RentCast API key for fetching property data
|
||||
ENV RENTCAST_API_KEY=""
|
||||
ENV RENTCAST_PAYEE_NAME="RentCast"
|
||||
|
||||
VOLUME ./cache
|
||||
|
||||
# Copy the current directory contents into the container at /usr/src/app
|
||||
|
||||
@@ -47,7 +47,7 @@ ACTUAL_CACHE_DIR="./cache"
|
||||
# optional, name of the payee for added interest transactions
|
||||
INTEREST_PAYEE_NAME="Loan Interest"
|
||||
|
||||
# optional, name of the payee for added interest transactions
|
||||
# optional, name of the payee for added investment transactions
|
||||
INVESTMENT_PAYEE_NAME="Investment"
|
||||
# optional, name of the category group for added investment tracking transactions
|
||||
INVESTMENT_CATEGORY_GROUP_NAME="Income"
|
||||
@@ -72,6 +72,23 @@ RENTCAST_API_KEY="<Rentcast API key>"
|
||||
RENTCAST_PAYEE_NAME="RentCast"
|
||||
```
|
||||
|
||||
### OIDC Auth Provider Support
|
||||
|
||||
When using OIDC, in your Actual Budget server config you must be able to log in
|
||||
with a password.
|
||||
|
||||
Set the following in your **server config** (not in your helpers config!), then
|
||||
on initial login you must set a password:
|
||||
|
||||
```
|
||||
ACTUAL_OPENID_AUTH_METHOD=openid
|
||||
ACTUAL_LOGIN_METHOD=openid
|
||||
ACTUAL_ALLOWED_LOGIN_METHODS=openid,password,header
|
||||
ACTUAL_OPENID_ENFORCE=false
|
||||
```
|
||||
|
||||
Then configure the helpers for password login, as shown above.
|
||||
|
||||
## Installation
|
||||
|
||||
Run `npm install` to install any required dependencies.
|
||||
@@ -168,7 +185,9 @@ interest transaction on the 28th of the month, set the account note to
|
||||
By default, interest is calculated using the 30/360 method where interest is
|
||||
computed monthly using 30/360 (or 1/12) of the interest rate. If you need to
|
||||
compute interest using the ACTUAL/ACTUAL method, set `interest:actual` in the
|
||||
note. If you need to compute interest daily, set `interest:daily`.
|
||||
note. If you need to compute interest daily, set `interest:daily`. For most
|
||||
student loans, you probably want simple dailiy interest
|
||||
`interest:daily-simple`.
|
||||
|
||||
You can optionally change the payee used for the interest transactions by
|
||||
setting `INTEREST_PAYEE_NAME` in the `.env` file.
|
||||
@@ -390,3 +409,43 @@ node sync-bitcoin.js
|
||||
```
|
||||
|
||||
It is recommended to run this script once per day or week.
|
||||
|
||||
### Tracking Crypto Prices
|
||||
|
||||
This script tracks the value of any cryptocurrency. Similar to the Bitcoin
|
||||
script, it adds new transactions to keep the account balance equal to the
|
||||
latest value. Values are retrieved from the Kraken API, but the script is
|
||||
customizable to use other APIs if needed.
|
||||
|
||||
To use, set these tags in the account notes:
|
||||
|
||||
- `CRYPTO:<symbol>,<symbol>,...` - where each symbol is a cryptocurrency
|
||||
you want to track, e.g. `CRYPTO:BTC,ETH,DOGE`
|
||||
- `<symbol>:<amount>[,<krakenPair>]` - where `<symbol>` is the cryptocurrency
|
||||
symbol and `<amount>` is the amount you own. If using Kraken, specify the Kraken
|
||||
pair to use for the price. Obtain this by visiting
|
||||
[https://api.kraken.com/0/public/Ticker?pair=<symbol\>usd](https://api.kraken.com/0/public/Ticker?pair=btcusd)
|
||||
and locating the key under `result` that matches your symbol.
|
||||
|
||||
For example, if you own 0.01 BTC and 1 ETH, your account note would look like:
|
||||
```
|
||||
CRYPTO:BTC,ETH
|
||||
BTC:0.01,XXBTZUSD
|
||||
ETH:1,XXETHZUSD
|
||||
```
|
||||
|
||||
To use a different API, set the following in your `.env` file: (example using Bitcoin)
|
||||
```shell
|
||||
CRYPTO_PRICE_URL_BTC="https://api.kraken.com/0/public/Ticker?pair=xbtgbp"
|
||||
CRYPTO_PRICE_JSON_PATH_BTC="result.XXBTZGBP.c[0]"
|
||||
```
|
||||
|
||||
To run:
|
||||
|
||||
```console
|
||||
node sync-crypto.js
|
||||
```
|
||||
|
||||
It is recommended to run this script once per day or week. If you are retrieving
|
||||
BTC prices using this script, DO NOT run the `sync-bitcoin.js` script as well, as it will
|
||||
set the account balance to the value of your BTC holdings only.
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
v25.5.0
|
||||
v26.5.2
|
||||
|
||||
+10
-2
@@ -41,12 +41,15 @@ function daysInYear(year) {
|
||||
|
||||
const lastDate = await getLastTransactionDate(account, cutoff);
|
||||
if (!lastDate) continue;
|
||||
const daysPassed = Math.floor((interestTransactionDate - new Date(lastDate)) / 86400000);
|
||||
const daysPassed = Math.round(
|
||||
(interestTransactionDate.setHours(0, 0, 0, 0) - new Date(lastDate).setHours(0, 0, 0, 0)) / 86400000
|
||||
);
|
||||
|
||||
let period = 12;
|
||||
let numPeriods = 1
|
||||
switch (kind) {
|
||||
case 'daily':
|
||||
case 'daily-simple':
|
||||
period = daysInYear(interestTransactionDate.getFullYear());
|
||||
numPeriods = daysPassed;
|
||||
break;
|
||||
@@ -58,7 +61,12 @@ function daysInYear(year) {
|
||||
}
|
||||
|
||||
const balance = await getAccountBalance(account, interestTransactionDate);
|
||||
const compoundedInterest = Math.round(balance * (Math.pow(1 + interestRate / period, numPeriods) - 1));
|
||||
|
||||
let compoundedInterest;
|
||||
if (kind == 'daily-simple')
|
||||
compoundedInterest = Math.round(balance * (interestRate / 365) * numPeriods);
|
||||
else
|
||||
compoundedInterest = Math.round(balance * (Math.pow(1 + interestRate / period, numPeriods) - 1));
|
||||
|
||||
interestRate = showPercent(interestRate);
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
const api = require('@actual-app/api');
|
||||
const { closeBudget, ensurePayee, getAccountBalance, getAccountNote, getTagValue, openBudget, showPercent, sleep,
|
||||
getBudgetMonth, getCategoryGroups, getCategories } = require('./utils');
|
||||
require("dotenv").config();
|
||||
|
||||
(async function () {
|
||||
await openBudget();
|
||||
|
||||
var today = new Date();
|
||||
var yyyy = today.getFullYear();
|
||||
var mm = (today.getMonth() + 1).toString().padStart(2, '0');
|
||||
var budgetMonth = yyyy + '-' + mm;
|
||||
const budget = await api.getBudgetMonth(budgetMonth);
|
||||
|
||||
console.log('');
|
||||
console.log('~~!!'); // Make it easy to parse for e-mail
|
||||
|
||||
console.log('Budget for Month: ' + budgetMonth);
|
||||
console.log('Total Budget: $' + (budget.totalBudgeted * .01).toFixed(2).padStart(8, ' '));
|
||||
console.log('Income: $' + (budget.totalIncome * .01).toFixed(2).padStart(8, ' '));
|
||||
console.log('Spent: $' + (budget.totalSpent * .01).toFixed(2).padStart(8, ' '));
|
||||
console.log('Balance: $' + (budget.totalBalance * .01).toFixed(2).padStart(8, ' '));
|
||||
console.log('----------------------------------------------------');
|
||||
|
||||
const categoryGroups = await api.getCategoryGroups();
|
||||
for (const categoryGroup of budget.categoryGroups) {
|
||||
if (categoryGroup.hidden || categoryGroup.budgeted == 0 || categoryGroup.is_income) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const categorySpentPercent = ((categoryGroup.spent * .01) / (categoryGroup.budgeted * .01) * -100);
|
||||
const categoryBalancePercent = ((categoryGroup.balance * .01) / (categoryGroup.budgeted * .01) * 10);
|
||||
|
||||
console.log(categoryGroup.name.padEnd(21, ' ') + ' ' + categorySpentPercent.toFixed(1).trim().padStart(7, ' ') + '% spent of $' + (categoryGroup.budgeted * .01).toFixed(2).trim().padStart(8, ' '));
|
||||
}
|
||||
|
||||
console.log('!!~~'); // Make it easy to parse for e-mail
|
||||
console.log('');
|
||||
|
||||
var transactionDate = new Date();
|
||||
transactionDate.setDate(transactionDate.getDate() - 1);
|
||||
let txDate = transactionDate.toISOString().substring(0, 10);
|
||||
console.log('Transactions on ' + txDate);
|
||||
console.log('DATE ACCOUNT PAYEE NOTES / MEMO AMOUNT');
|
||||
console.log('-------------------------------------------------------------------------------------------------------------------------');
|
||||
const payees = await api.getPayees();
|
||||
const accounts = await api.getAccounts();
|
||||
( await api.runQuery(
|
||||
api.q('transactions')
|
||||
.filter({
|
||||
date: { $gte: txDate },
|
||||
})
|
||||
.select('*')
|
||||
)
|
||||
).data.map(row => {
|
||||
let payeeName = payees.find(p => p.id === row.payee)?.name;
|
||||
let accountName = accounts.find(a => a.id === row.account)?.name;
|
||||
console.log(row.date + ' ' +
|
||||
fixedStringLength(accountName, 20) + ' ' +
|
||||
fixedStringLength(payeeName, 25) + ' ' +
|
||||
fixedStringLength(row.notes, 45) + ' ' +
|
||||
('$' + (row.amount / 100).toFixed(2).trim()).padStart(11, ' '));
|
||||
});
|
||||
|
||||
|
||||
await closeBudget();
|
||||
})();
|
||||
|
||||
function fixedStringLength(str, length) {
|
||||
if (str === null) {
|
||||
str = '';
|
||||
}
|
||||
return str.padEnd(length).substring(0, length);
|
||||
}
|
||||
+1
-1
@@ -13,7 +13,7 @@ ACTUAL_CACHE_DIR="./cache"
|
||||
# optional, name of the payee for added interest transactions
|
||||
INTEREST_PAYEE_NAME="Loan Interest"
|
||||
|
||||
# optional, name of the payee for added interest transactions
|
||||
# optional, name of the payee for added investment transactions
|
||||
INVESTMENT_PAYEE_NAME="Investment"
|
||||
# optional, name of the category group for added investment tracking transactions
|
||||
INVESTMENT_CATEGORY_GROUP_NAME="Income"
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
# Require APT to utilizelocal proxy
|
||||
echo "Acquire::http::proxy \"http://192.168.128.185:3142\";" >> /etc/apt/apt.conf.d/02proxy
|
||||
|
||||
# Set TimeZone :)
|
||||
timedatectl set-timezone America/Chicago
|
||||
|
||||
# Perform update / upgrade of all existing packages and repos
|
||||
apt update && apt upgrade -y
|
||||
|
||||
# Install curl and git if not present
|
||||
apt install curl git ca-certificates fonts-liberation libasound2 libatk-bridge2.0-0 libatk1.0-0 \
|
||||
libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libglib2.0-0 \
|
||||
libgtk-3-0 libnspr4 libnss3 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 \
|
||||
libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 \
|
||||
libxrandr2 libxrender1 libxss1 libxtst6 lsb-release wget xdg-utils \
|
||||
apache2 php php-curl php-json php-cgi libapache2-mod-php -y
|
||||
|
||||
# Add NodeSource repository (replace 22.x with your desired version, e.g., 20.x, 18.x)
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
||||
|
||||
# Install Node.js
|
||||
apt install -y nodejs
|
||||
|
||||
# Git clone the actual-helpers repo
|
||||
cd /opt
|
||||
git clone https://git.kollman.net/rlkollman/actual-helpers
|
||||
|
||||
# Install all dependencies
|
||||
cd /opt/actual-helpers
|
||||
npm install
|
||||
|
||||
# Write crontab file with scheduled tasks
|
||||
echo "" >> /etc/crontab
|
||||
echo "# Sync ActualBudget Bank Accounts daily at 2 AM" >> /etc/crontab
|
||||
echo "00 2 * * * root cd /opt/actual-helpers && node sync-banks.js > /dev/null" >> /etc/crontab
|
||||
echo "05 2 * * * root cd /opt/actual-helpers && node update-investments.js > /dev/null" >> /etc/crontab
|
||||
echo "" >> /etc/crontab
|
||||
echo "# Sync ActualBudget Vehicles & Apply Mortgage Interest on the 1st day of every month at 3 AM" >> /etc/crontab
|
||||
echo "00 3 1 * * root cd /opt/actual-helpers && node kbb.js > /dev/null" >> /etc/crontab
|
||||
echo "15 3 6 * * root cd /opt/actual-helpers && node apply-interest.js > /dev/null" >> /etc/crontab
|
||||
echo "" >> /etc/crontab
|
||||
echo "# Sync ActualBudget Residence through RentCast on the 1st & 15th day of every month at 4 AM" >> /etc/crontab
|
||||
echo "#00 4 1 * * root cd /opt/actual-helpers && node rentcast.js > /dev/null" >> /etc/crontab
|
||||
echo "#00 4 15 * * root cd /opt/actual-helpers && node rentcast.js > /dev/null" >> /etc/crontab
|
||||
echo "" >> /etc/crontab
|
||||
echo "# Sync ActualBudget Residence through Zillow on the 1st & 16th day of every month at 5 AM" >> /etc/crontab
|
||||
echo "30 4 1 * * root cd /opt/actual-helpers && node zestimate.js > /dev/null" >> /etc/crontab
|
||||
echo "30 4 16 * * root cd /opt/actual-helpers && node zestimate.js > /dev/null" >> /etc/crontab
|
||||
echo "" >> /etc/crontab
|
||||
echo "# Output the current budget status to a file" >> /etc/crontab
|
||||
echo "00 5 * * * root cd /opt/actual-helpers && node budget.js > /root/budget.txt" >> /etc/crontab
|
||||
Generated
+1186
-249
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@actual-app/api": "latest",
|
||||
"better-sqlite3": "^11.7.2",
|
||||
"chromedriver": "^131.0.4",
|
||||
"dotenv": "^16.4.5",
|
||||
"jsdom": "^24.1.0",
|
||||
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
const { closeBudget, openBudget, getTransactions, getAccountNote, getAccountBalance, getTagValue, ensurePayee } = require('./utils');
|
||||
const api = require('@actual-app/api');
|
||||
|
||||
function getValueAtPath(obj, path) {
|
||||
const keys = path.split('.').filter(Boolean);
|
||||
|
||||
return keys.reduce((acc, key) => {
|
||||
const match = key.match(/^([^\[\]]+)(\[(\d+)\])?$/);
|
||||
|
||||
if (match) {
|
||||
const property = match[1];
|
||||
const index = match[3];
|
||||
|
||||
acc = acc[property];
|
||||
|
||||
if (index !== undefined) {
|
||||
acc = acc[parseInt(index, 10)];
|
||||
}
|
||||
} else {
|
||||
acc = acc[key];
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, obj);
|
||||
}
|
||||
|
||||
async function getCryptoPrice(crypto, krakenPath) {
|
||||
const url = process.env[`CRYPTO_PRICE_URL_${crypto.toUpperCase()}`] || `https://api.kraken.com/0/public/Ticker?pair=${crypto}usd`;
|
||||
if (!krakenPath && !process.env[`CRYPTO_PRICE_JSON_PATH_${crypto.toUpperCase()}`]) {
|
||||
console.error(`No Kraken path provided for ${crypto}. Please set CRYPTO_PRICE_JSON_PATH_${crypto.toUpperCase()} environment variable or provide krakenPath argument.`);
|
||||
return undefined;
|
||||
}
|
||||
const path = process.env[`CRYPTO_PRICE_JSON_PATH_${crypto.toUpperCase()}`] || `result.${krakenPath}.c[0]`;
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const json = await response.json();
|
||||
return getValueAtPath(json, path);
|
||||
} catch (error) {
|
||||
console.error(`Error fetching price for ${crypto}:`, error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
(async () => {
|
||||
await openBudget();
|
||||
const payeeId = await ensurePayee(process.env.CRYPTO_PAYEE_NAME || 'Crypto Price Change');
|
||||
const accounts = await api.getAccounts();
|
||||
for (const account of accounts) {
|
||||
if (account.closed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Need to set cutoff date to tomorrow to ensure we get the latest transactions
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setDate(cutoffDate.getDate() + 1);
|
||||
|
||||
const note = await getAccountNote(account, cutoffDate);
|
||||
|
||||
// Get cryptos list from the note
|
||||
let cryptos = getTagValue(note, 'CRYPTO', null);
|
||||
if (!cryptos) continue;
|
||||
cryptos = cryptos.split(',').map(c => c.trim()).filter(c => c.length > 0);
|
||||
if (cryptos.length === 0) continue;
|
||||
|
||||
console.log(`Processing account: ${account.name}, Cryptos: ${cryptos.join(', ')}`);
|
||||
|
||||
let totalTargetBalance = 0;
|
||||
for (const crypto of cryptos) {
|
||||
const cryptoTag = getTagValue(note, crypto, 0.0);
|
||||
if (!cryptoTag) continue;
|
||||
cryptoTagSplit = cryptoTag.split(',');
|
||||
|
||||
// Get the amount from the first part of the tag
|
||||
amount = parseFloat(cryptoTagSplit[0]);
|
||||
if (isNaN(amount) || amount <= 0) continue;
|
||||
console.log(`Crypto: ${crypto}, Amount: ${amount}`);
|
||||
|
||||
// Get Kraken path from the second part of the tag, if it exists
|
||||
let krakenPath = cryptoTagSplit[1]
|
||||
console.log(`Kraken Path: ${krakenPath}`);
|
||||
|
||||
// Get the crypto price
|
||||
const cryptoPrice = await getCryptoPrice(crypto, krakenPath);
|
||||
if (!cryptoPrice) {
|
||||
console.error(`Unable to retrieve price for ${crypto}. Check your CRYPTO_PRICE_URL_${crypto.toUpperCase()} and CRYPTO_PRICE_JSON_PATH_${crypto.toUpperCase()} environment variables`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`Crypto: ${crypto}, Price: ${cryptoPrice}`);
|
||||
|
||||
const targetBalance = Math.round(cryptoPrice * amount * 100);
|
||||
totalTargetBalance += targetBalance;
|
||||
}
|
||||
|
||||
// Now we need to check if the total target balance is different from the current balance and update it
|
||||
const currentBalance = await getAccountBalance(account, cutoffDate);
|
||||
const diff = totalTargetBalance - currentBalance;
|
||||
console.log(`Account: ${account.name}, Total Target Balance: ${totalTargetBalance}, Current Balance: ${currentBalance}, Diff: ${diff}`);
|
||||
if (diff != 0) {
|
||||
await api.importTransactions(account.id, [{
|
||||
date: new Date(),
|
||||
payee: payeeId,
|
||||
amount: diff,
|
||||
cleared: true,
|
||||
reconciled: true,
|
||||
notes: `Updated Crypto Price on ${new Date().toLocaleString()} (${cryptos.join(', ')})`,
|
||||
}])
|
||||
}
|
||||
}
|
||||
await closeBudget();
|
||||
})();
|
||||
@@ -16,6 +16,7 @@ const getCredentials = async () => {
|
||||
const response = await fetch(url, { method: 'post' });
|
||||
const api_url = await response.text();
|
||||
|
||||
try {
|
||||
const rest = api_url.split('//', 2)[1];
|
||||
const auth = rest.split('@', 1)[0];
|
||||
const username = auth.split(':')[0];
|
||||
@@ -26,6 +27,10 @@ const getCredentials = async () => {
|
||||
fs.writeFileSync(cache + '/simplefin.credentials', data);
|
||||
console.log('SimpleFIN credentials:', data);
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.log('Invalid SimpleFIN setup response:', api_url);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const loadCredentials = () => {
|
||||
@@ -41,6 +46,9 @@ const getSimplefinBalances = async () => {
|
||||
let credentials = loadCredentials();
|
||||
if (!credentials) {
|
||||
credentials = await getCredentials();
|
||||
if (!credentials) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
const username = credentials.split(':')[0];
|
||||
const pw = credentials.split(':')[1];
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
const api = require('@actual-app/api');
|
||||
const fs = require('fs');
|
||||
const readline = require('readline');
|
||||
const { closeBudget, ensurePayee, getAccountBalance, getAccountNote, getTagValue, openBudget, showPercent, sleep,
|
||||
getBudgetMonth, getCategoryGroups, getCategories } = require('./utils');
|
||||
require("dotenv").config();
|
||||
|
||||
(async function () {
|
||||
await openBudget();
|
||||
|
||||
const fileStream = fs.createReadStream("/var/www/html/data/investment-data.txt");
|
||||
const rl = readline.createInterface({
|
||||
input: fileStream,
|
||||
crlfDelay: Infinity
|
||||
});
|
||||
|
||||
const payeeId = await ensurePayee('Change in Market Value');
|
||||
for await (const line of rl) {
|
||||
const parts = line.split('=');
|
||||
const accountBalance = await api.getAccountBalance(parts[0]);
|
||||
const adjustment = (accountBalance - parts[1].replace(/\./g, '')) * -1;
|
||||
if (adjustment == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(parts[0] + ' --> ' + parts[1] + ' --> $' + adjustment);
|
||||
|
||||
await api.importTransactions(parts[0], [{
|
||||
date: new Date(),
|
||||
payee: payeeId,
|
||||
amount: adjustment,
|
||||
cleared: true,
|
||||
reconciled: true,
|
||||
notes: `Change in Market Value`,
|
||||
}]);
|
||||
|
||||
}
|
||||
|
||||
deleteFileIfExists('/var/www/html/data/investment-data.txt');
|
||||
await closeBudget();
|
||||
})();
|
||||
|
||||
function fixedStringLength(str, length) {
|
||||
if (str === null) {
|
||||
str = '';
|
||||
}
|
||||
return str.padEnd(length).substring(0, length);
|
||||
}
|
||||
|
||||
async function deleteFileIfExists(filePath) {
|
||||
try {
|
||||
await fs.promises.unlink(filePath);
|
||||
console.log(`File ${filePath} deleted successfully.`);
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
console.log(`File ${filePath} does not exist, so it was not deleted.`);
|
||||
} else {
|
||||
console.error('Error deleting file:', err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
-11
@@ -1,35 +1,55 @@
|
||||
const { Builder, Browser, By, until } = require('selenium-webdriver')
|
||||
const puppeteer = require('puppeteer-extra');
|
||||
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
|
||||
const api = require('@actual-app/api');
|
||||
const { closeBudget, ensurePayee, getAccountBalance, getAccountNote, getTagValue, openBudget, showPercent, sleep } = require('./utils');
|
||||
require("dotenv").config();
|
||||
|
||||
puppeteer.use(StealthPlugin());
|
||||
|
||||
async function getZestimate(URL) {
|
||||
let driver = await new Builder()
|
||||
.forBrowser(Browser.CHROME)
|
||||
.build();
|
||||
const browser = await puppeteer.launch({
|
||||
headless: 'new',
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
|
||||
try {
|
||||
await driver.get(URL);
|
||||
const html = await driver.wait(until.elementLocated(By.css('body')), 5000).getAttribute('innerHTML');
|
||||
|
||||
await page.goto(URL, { waitUntil: 'domcontentloaded', timeout: 20000 });
|
||||
const html = await page.content();
|
||||
try {
|
||||
let match = html.match(/"zestimate":"(\d+)"/);
|
||||
console.log('Got html data!');
|
||||
let match = html.match(/"price":"(\d+)"/);
|
||||
if (match) {
|
||||
console.log('matched 1st');
|
||||
return parseInt(match[1]) * 100;
|
||||
}
|
||||
match = html.match(/\\"zestimate\\":\\"(\d+)\\"/);
|
||||
match = html.match(/\\"price\\":(\d+)/);
|
||||
if (match) {
|
||||
console.log('matched 2nd');
|
||||
return parseInt(match[1]) * 100;
|
||||
}
|
||||
match = html.match(/\\"price\\":\\"(\d+)\\"/);
|
||||
if (match) {
|
||||
console.log('matched 3rd');
|
||||
return parseInt(match[1]) * 100;
|
||||
}
|
||||
console.log('didn\'t match any :(');
|
||||
console.log('~~!!');
|
||||
// console.log(html);
|
||||
console.log('!!~~');
|
||||
} catch (error) {
|
||||
console.log('Error parsing Zillow page:');
|
||||
console.log(error);
|
||||
console.log(html);
|
||||
}
|
||||
} catch (er) {
|
||||
console.log('Error while fetching zestimate:');
|
||||
console.log(er);
|
||||
console.lo(html);
|
||||
} finally {
|
||||
await driver.quit();
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user