Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ The tab bar is Start, Compare, Usage, Bill?, Free? and Solar+?. Hovering a tab s
### Compare

- Pick a plan to compare with: pre-built templates for each state (including 2026-27 Default Market Offer and Victorian Default Offer rates, market offers, and the Solar Sharer Offer), or your own flat or time-of-use plan with controlled load, optional demand tariffs, time-varying feed-in (for example Synergy DEBS 10c peak / 2c off-peak) with optional daily kWh caps, and free-window plans (a free or cheap window with an optional daily cap, a super off-peak / EV window, and seasonal peak windows).
- The answer for the chosen dates, every plan for your network ranked, and month by month. Demand charges are worked out per calendar month from each month's own peak, and Ausgrid EA029 exports are settled as on the bill (free midday allowance, export charge on usage). GST can be shown or hidden.
- The answer for the chosen dates, every plan for your network ranked, and month by month. Demand charges are worked out per calendar month from each month's own peak, and Ausgrid EA029 exports are settled as on the bill (free midday allowance, export charge on usage), one calendar month at a time, so the answer, month by month and the Free? tab give the same Amber total. GST can be shown or hidden.
- Download every meter reading, daily totals or the comparison table as CSV.

### Usage
Expand All @@ -56,7 +56,7 @@ The tab bar is Start, Compare, Usage, Bill?, Free? and Solar+?. Hovering a tab s

The solar and battery planner. From the last 12 months of meter readings (Amber's, an imported Amber CSV, or a NEM12 file) it tries every mix of new solar size, battery size and plan for the network, half hour by half hour, and ranks them by 20-year value, payback or yearly bill.

- **What you can set**: panels, battery and plan can each be "find the best", "a size I choose", "none", or one plan. Companies, or single plans, can be ticked or unticked; only ticked plans are searched and shown. Roof size, facing, pitch, shade, town, export limit, whether panels switch off at negative feed-in prices, battery round trip and power, prices and rebates, wear and replacement, price rises, the interest rate and planned changes (an electric car, heat-pump hot water, gas heating and cooking to electric with the gas bill saved).
- **What you can set**: panels, battery and plan can each be "find the best", "a size I choose", "none", or one plan. Companies, or single plans, can be ticked or unticked; only ticked plans are searched and shown. Roof size, facing, pitch, shade, town, export limit, whether panels switch off at negative feed-in prices, battery round trip and power, prices and rebates (the battery rebate is tiered by size: the full rate on the first 14 kWh, 60% from 14 to 28 kWh, 15% from 28 to 50 kWh), wear and replacement, price rises, the interest rate and planned changes (an electric car, heat-pump hot water, gas heating and cooking to electric with the gas bill saved).
- **The yardstick**: every option is measured against nothing new on the plan you compare against; by default that's whichever ticked plan is cheapest for you with nothing new, since switching is free.
- **What it shows**:
- the best option, with what to ask installers for (panel, inverter and battery sizes; a "curtailable" inverter where the feed-in price goes below zero, and on Amber a battery SmartShift can run);
Expand All @@ -78,14 +78,16 @@ The solar and battery planner. From the last 12 months of meter readings (Amber'
## Data completeness and CSV backups

- When the selected dates contain days without readings, Compare labels the result **Incomplete comparison**. Usage costs leave out missing readings, while fixed charges and export allowances cover the full period, so savings are provisional until the missing readings arrive. The calendar labels missing days **No readings** rather than treating them as zero use. Daily CSV and calendar totals cover only dates with readings and may differ from an incomplete full-period comparison.
- Daily free-power and feed-in caps are shared across the household's meter channels. Ausgrid export settlement adjustments are allocated to days in proportion to midday exports, so daily totals reconcile with the period's settlement; they aren't separate daily allowances.
- Daily free-power and feed-in caps are shared across the household's meter channels. Ausgrid export settlement adjustments are allocated to days in proportion to midday exports, so daily totals reconcile with each month's settlement; they aren't separate daily allowances. The free allowance is counted per calendar month (a bill is checked as one settlement of its own), so a quiet month doesn't lend its allowance to a sunny one.
- **Download → Every meter reading** produces a versioned CSV that can be restored through the Amber usage CSV import on the Start tab, for the same Amber site. It keeps the original NEM interval-end times, duration, quality, renewables and demand flags, including zero-use channels. Whole-day backups can restore recent and estimated readings; cached final API days are kept, and estimated readings can still be refreshed from the API. Incomplete days aren't restored. App exports from before the version column aren't supported as backups (Amber's own usage export is). For a non-Amber household, keep its original NEM12 file to re-import.

## Privacy

- The API key is stored only in this browser (localStorage, or sessionStorage if **Remember key on this device** is unticked). It is obfuscated, not encrypted.
- Usage data is requested directly from `https://api.amber.com.au` and never sent anywhere else; the page's Content Security Policy allows no other connections. Imported files (Amber CSV, NEM12, bills) are read in the browser and stay there.
- Meter readings are cached in IndexedDB; settings, saved plans, imported households and the last checked bill (until **Clear**) are in localStorage. The settings menu (cog) can clear the cache and saved plans.
- Meter readings are cached in IndexedDB; settings, saved plans, imported households and the last checked bill (until **Clear**) are in localStorage. The settings menu (cog) can clear the cache and saved plans. Clearing the saved meter readings also removes imported Amber CSV history and households imported from NEM12, which then need importing again.
- The state follows your Amber site's network when the state showing has no plans for it (a Victorian site opens on VIC, not NSW). Changing state swaps a built-in plan for one from the new state; rates you typed yourself stay. If you pick a state with no plans for your network, a note says which state has them.
- A built-in plan you've selected keeps any prices you change yourself, but when a new version of the app changes that plan's published prices, the saved form is filled again from the new prices.

## For developers

Expand Down
2 changes: 1 addition & 1 deletion css/app.css

Large diffs are not rendered by default.

13 changes: 8 additions & 5 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ Imports (Amber CSV, NEM12 estimate) produce the same shape, so every feature wor

### The cache

IndexedDB `AmberDataCacheDB`, store `dailyUsage`, one record per site and NEM date (`id = "<siteId>_<date>"`). Compare reads the cache first and fetches only missing or stale days, in 7-day chunks, 3 at a time (`Amber.FETCH_CHUNK_DAYS`, `FETCH_CONCURRENCY`). Final whole days are cached. Old days with no data are remembered as empty (`emptyDayCutoff`). Estimated readings stay eligible for refetch (`cachedDayUsable`).
IndexedDB `AmberDataCacheDB`, store `dailyUsage`, one record per site and NEM date (`id = "<siteId>_<date>"`). Connections aren't closed after use; each closes itself on `versionchange`, so "Clear saved meter readings" (`deleteDatabase`) isn't blocked. Every transaction rejects its promise on `abort` as well as `error` (a full disk aborts without an error event), and a Compare whose readings can't be saved still shows its results. Compare reads the cache first and fetches only missing or stale days, in 7-day chunks, 3 at a time (`Amber.FETCH_CHUNK_DAYS`, `FETCH_CONCURRENCY`). Final whole days are cached. Old days with no data are remembered as empty (`emptyDayCutoff`). Estimated readings stay eligible for refetch (`cachedDayUsable`).

### Channel totals

Expand Down Expand Up @@ -134,15 +134,15 @@ Missing days (up to 31) are filled from the nearest whole day on the same weekda

### Start (`app.js`: `setupStartTab`, imports, sites)

- API key: `Amber.setApiKey` stores an obfuscated key in localStorage (or sessionStorage); `Amber.fetchSites` lists sites.
- API key: `Amber.setApiKey` stores an obfuscated key in localStorage (or sessionStorage); `Amber.fetchSites` lists sites. The state selector follows the site's network (`siteNetworkState`, from `Amber.NETWORKS`) when the state showing has no plans for it, unless the user picked the state by hand for that site.
- Amber's fixed charges: `loadAmberChargeFields` / `readAmberRates`. The two years' connection, subscription and demand rates are split at `Amber.AMBER_RATES_CHANGE_DATE` (1 July 2026). Uploading a bill (`handleAmberBillFile` → `bill.js`) fills the year it belongs to.
- Amber CSV import: `handleUsageCsvFile` → `import.js` → `setUsageDataMany`.
- NEM12: `handleNem12File` → `estimate.js` parses the file and rebuilds Amber's interval prices from `data/prices` and `networks.js`. The result becomes a local site whose days are written to the cache.

### Compare (`app.js`: `fetchAndCompare`, `displayResults`, `renderAllPlansTable`, `renderMonthly`)

1. Read the cache for the dates, fetch what's missing (`api.js`) and write final days back.
2. `processUsageData` → channel totals; `applyAmberFeedInSettlement` (Ausgrid EA029 export settlement).
2. `processUsageData` → channel totals; `applyAmberFeedInSettlement` (Ausgrid EA029 export settlement, one calendar month at a time via `Amber.monthsInRange`; Bill? settles the bill's own period with `billPeriod`).
3. `calculateOtherSupplierCosts` (the chosen plan), `calculateDemandTariff` / `calculateOtherDemandTariff` (monthly peaks), `precalculateDailySummaries` (daily rows and calendar).
4. Render the answer, every plan for the network (`computePlanTotals` per template), then month by month (`monthly.js`, after the main results).

Expand Down Expand Up @@ -181,7 +181,7 @@ Key pieces:
- **Battery** (`simulateBattery`): one day at a time with that day's prices known (perfect daily foresight). It searches a charge threshold and a discharge threshold per day. It charges from solar over the export cap first, then from cheap or free grid; it discharges into dear use and high feed-in. Charge and discharge share the power limit, and a small wear cost per kWh stops pointless cycling. `limited` counts what's thrown away at the export limit.
- **Existing battery**: `plannerDetectExisting` guesses panels and battery from the readings. `plannerUnwindBattery` removes the battery's effect (grid sales are seen directly; what it covered at home is estimated), and the search re-runs it on every plan with any new battery. `plannerExistingCheck` compares the planner's version with the real readings.
- **Search** (`plannerSearch`): every solar size on every plan without a battery. A probe battery ranks plans, then the full battery search runs on the best `keepPlans` (6) plus any that must be included. `allowPlans` limits the plans considered to those ticked.
- **Money** (`plannerCapex`, `plannerFinance`): the up-front cost after rebates. Then year by year: savings grow with price rises and shrink with panel and battery wear, the battery and inverter are replaced, and a battery replaced inside the period is credited with its remaining life. The results are NPV, IRR, payback "ahead for good" (and the first time it reaches $0), and `plannerWaitAYear`.
- **Money** (`plannerCapex`, `plannerFinance`): the up-front cost after rebates (the battery rebate follows `Amber.BATTERY_REBATE_TIERS`: full to 14 kWh, 60% to 28 kWh, 15% to 50 kWh). Then year by year: savings grow with price rises and shrink with panel and battery wear, the battery and inverter are replaced, and a battery replaced inside the period is credited with its remaining life. The results are NPV, IRR, payback "ahead for good" (and the first time it reaches $0), and `plannerWaitAYear`.
- **Ratings** (`plannerKeenLevel`): the value per $1 up front (≥1.75 Strong, ≥1.4 Good, ≥1.15 Modest). Strong and Good also need the result to hold with prices 20% lower and with 10am–3pm feed-in halved (`dayFeedInScale`), and payback within 8 years.
- **UI** (`planner-ui.js`): `search()` gathers inputs, `analyse()` builds options against the yardstick, and the `render*` functions build each card as HTML strings (escaped with `esc`).

Expand All @@ -193,13 +193,16 @@ Key pieces:
| localStorage | `amberApiKey` (or sessionStorage) | Obfuscated API key |
| localStorage | `selectedSiteId`, `startDate`, `endDate`, `activeTab` | Where the user was |
| localStorage | `selectedState`, `selectedPlan`, `rateType`, `planName`, `flatRate`, `flatFeedInRate`, `flatControlledLoad`, `dailyConnectionRate`, `touConfig`, `feedInWindows`, `enableFeedInTou`, `demandSettings`, `ratesDetailsOpen` | The Compare plan form |
| localStorage | `selectedPlanSig` | Fingerprint (`Amber.templateSignature`) of the built-in plan the form was filled from. If it no longer matches on load, the plan's prices have changed and the form is filled again |
| localStorage | `customSupplierPlans` | Saved custom plans |
| localStorage | Amber charge field IDs | Amber's fixed charges (this year and last) |
| localStorage | `localSites` | Imported NEM12 households |
| localStorage | `batteryShiftSettings` | Free? tab settings |
| localStorage | `amberBillCheck` | The last bill check |
| localStorage | `plannerSettings` | Every Solar+? input and the company ticks |

Saved JSON is read through `storedJson` / `storedCustomPlans` in `app.js`: a value that is missing, damaged or the wrong shape falls back to the default instead of stopping `init()`.

## External data

- **AEMO prices**: `scripts/update-prices.js` downloads `PRICE_AND_DEMAND_<YYYYMM>_<REGION>.csv` from AEMO into `data/prices/<REGION>/<YYYY-MM>.json` as `{ region, month, interval: 5, rrp: [...] }` ($/MWh, interval ending `month-01 00:00 + (i+1)×5 min` NEM time). `.github/workflows/update-prices.yml` runs it daily at 1:17 am AEST and commits changes.
Expand All @@ -210,7 +213,7 @@ Key pieces:

- The CSP (`index.html` `<meta http-equiv="Content-Security-Policy">`) is `default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self' https://api.amber.com.au; img-src 'self' data:; …`. This means no inline scripts, no inline `<style>` and no `style="…"` attributes (setting `element.style.x` from JS is fine), and no third-party hosts.
- All data stays in the browser. The API key is obfuscated (XOR with a fixed key, then base64), not encrypted.
- Dynamic HTML is built with template strings. Anything from data or user input goes through `Amber.escapeHTML` (aliased `esc` in `planner-ui.js`).
- Dynamic HTML is built with template strings. Anything from data or user input goes through `Amber.escapeHTML` (aliased `esc` in `planner-ui.js`), which escapes `& < > " '`, so it is safe inside an element and inside a quoted attribute.

## Build, test and CI

Expand Down
Loading
Loading