← Back to about

SAMS SOLAR

Release notes

# Changelog

All notable changes to this project are documented here. Versioning follows
[Semantic Versioning](https://semver.org/): `MAJOR.MINOR.PATCH` — patch for
bug fixes, minor for new features, major for breaking changes.

## [2.5.2] - 2026-08-08

### Fixed
- **Notification icon** — status-bar icon now shows a proper white sun silhouette instead of a white circle. Added monochrome `ic_notification` vector drawable; both FCM and `flutter_local_notifications` now reference it.

## [2.5.1] - 2026-08-07

### Fixed
- **Forecast deviation always showed green/positive** — API returns `pct` as an absolute value with a separate `direction` field ("above"/"below"); the app ignored `direction` so deviation was permanently `+X%` in green even when below forecast. Now signed correctly: negative and red when below forecast.
- **Deviation label and detail** — chip now shows "Above forecast" / "Below forecast" label and includes kWh alongside % (e.g. `+12% (1.2 kWh)`) to match the web dashboard.

## [2.5.0] - 2026-08-07

### Added
- **Prev/next day arrows** in Day tab — navigate one day at a time without the date picker
- **Peak power annotation** on power line charts — shows peak W and the time it occurred
- **Bill screen: zero-credit button** — one tap resets carry-forward credit to 0 (for DISCOM annual reset in April/May); persists to SharedPreferences
- **Bill screen: copy-to-next-cycle** — arrow button in results copies export excess to carry-forward credit field and persists it
- **Bill screen: save bill start date** — admin-gated button to update the server's last bill date from the app
- **Dashboard last-refreshed timestamp** — shows "Refreshed Xs ago" at the top of the dashboard
- **FCM token debug** in Settings — "Show notification token" dialog with selectable text for copying; token is also deleted from server on reset
- **Theme persistence** — chosen theme (light/dark/system) survives cold restarts via SharedPreferences
- `DELETE /api/app/token` backend endpoint — lets the app deregister a specific FCM token from the server
- **Grid meter stale detection** — backend marks home meter `ok=False` and nulls `power_w` if not updated in 10+ minutes; no stale values shown on dashboard

### Fixed
- All hardcoded `Colors.white*` in charts, dashboard, and card_header widgets — text, legends, grid lines, and axis labels are now visible in light theme
- `_FarmDailyChart` now uses `const` constructor

### Changed
- `/api/daily-deye-breakdown` default changed from 1500 → 365 days

## [1.6.0] - 2026-08-06

### Added
- **Android Flutter app** (`flutter_app/`) — 5-tab dashboard (Overview, Home, Farm, History, Settings)
  with live polling, battery SOC, grid meter, history charts, in-app APK update, and admin broadcast
- **FCM push notifications** — background push to Android app using Firebase Cloud Messaging V1 API;
  all existing VAPID browser alerts now also send to registered app tokens via new `send_push()` helper
- `POST /api/app/register-token` — app registers its FCM token (with app version) on startup
- `GET /api/app/version` — returns server version, build number, and APK download URL
- `GET /api/app/tokens` — admin endpoint listing all registered tokens with last-seen time
- `POST /api/app/broadcast` — admin endpoint to push a message to all app users
- `POST /api/app/notify-update` — called by CI after APK deploy to push update notification
- `FcmToken` table — stores FCM tokens with `app_version` and `last_seen` columns
- GitHub Actions `build-apk.yml` — builds release APK, deploys to `samssolar.in/downloads/`, writes version manifest JSON, notifies update
- `FCM_SERVICE_ACCOUNT_JSON` config env var for Firebase service account credentials
- `APK_DOWNLOAD_URL` and `APP_BUILD_NUMBER` config env vars for `/api/app/version` response

### Changed
- All poller alert calls (`push_notify.send_to_all`) replaced with `send_push` — now reaches both browser and app subscribers
- Backend version bumped 1.5.2 → 1.6.0

## [1.5.2] - 2026-07-13

Deye array grew from 6 to 7 panels (a 7th 600W panel added to the same
string) - updated `capacity_kw` in `app/array_specs.py` from 3.6 to 4.2 kW.
Only affects the forecast model going forward (morning predictions already
frozen for past days are untouched, nothing to backfill) - the model scales
linearly with capacity so no other recalibration needed.

## [1.5.1] - 2026-07-11

Fixed slow background API calls found via a GTmetrix report - the page
itself scored Grade A/85% with every Core Web Vital green, but "Fully
Loaded Time" was 30.4s because several endpoints (`/api/summary`,
`/api/daily`, `/api/daily-home-meter-breakdown`) each took 14-26s. Root
cause: those queries pulled every raw reading row (all ~40 columns) into a
full ORM object for the whole date range and aggregated in Python, with no
composite index on `(source, timestamp)` - the filter every one of them
uses. Also found `/api/daily?days=1500` was being fetched twice per page
load by two separate dashboard widgets that happened to want the same URL,
plus a third overlapping fetch of `/api/daily?days=3650` for the
year-over-year chart.

Fixed by adding the missing composite index, narrowing the three hot
queries (`daily_energy_totals`, `daily_deye_breakdown`,
`home_meter_daily_breakdown`) to only pull the 2-7 columns each actually
uses instead of all ~40, and deduplicating the frontend so Growatt Home,
Growatt Farm, and the year-over-year chart share one cached fetch instead
of three separate full-history scans. No change to any returned value or
chart - `test_db_aggregation.py`'s 7 tests pass unchanged, confirming the
column-narrowing didn't alter any aggregation result.

## [1.5.0] - 2026-07-11

Four of the five remaining items from the improvements list (external
uptime monitoring still pending - needs a decision on where its SMTP
credentials live):

- **Forecast accuracy chart** - predicted-vs-actual trend per source,
  reusing the morning forecasts already frozen and stored every day (no
  new storage needed). Makes calibration drift (panel aging, seasonal
  effects) visible over time instead of only ever comparing today
  against itself.
- **Multi-day forecast** - the morning job now fetches a 3-day weather
  forecast instead of 1 and splits it by calendar date. Today's slice is
  unchanged (still the frozen, self-correcting forecast) - the extra 2
  days are a preview that gets refined each morning until the day
  arrives and freezes for real. Shown as "Next: Sun 16.1 kWh · Mon 16.1
  kWh" under each source's forecast.
- **Weather-aware heads-up** - every 2 hours during daylight, re-checks
  live weather against what the frozen morning forecast assumed for the
  *remaining* hours of today. If a fresh check predicts significantly
  less (>25% lower) for the rest of the day, alerts immediately via
  email + push, rather than only finding out after the fact via the
  existing end-of-day deviation report.
- **Configurable alert thresholds** - zero-output minutes, low-battery
  SOC%, and source-down minutes can now be changed from the dashboard
  (admin-gated) and take effect immediately, no redeploy. Falls back to
  the .env default when no override is set (`app/runtime_settings.py`).

56 tests now (was 48), covering the new deterioration-detection logic
and the threshold override/fallback behavior.

## [1.4.0] - 2026-07-11

Completes the "Reliability / ops" items from the earlier improvements list:

- **Automated database backups** - a daily gzip-compressed snapshot of the
  entire SQLite database, emailed (not to the general subscriber list -
  it's a raw DB dump) at `BACKUP_HOUR` (default 3am). Uses SQLite's online
  backup API, safe to run against the live WAL-mode database without
  corruption risk. Previously the only copy of all history lived on one
  disk on one VPS with no backup at all.
- **Source-down alerting** - alerts if a source hasn't had a *successful*
  poll in `SOURCE_DOWN_ALERT_MINUTES` (default 20), distinct from
  zero-output (which only fires during daylight and can't tell a broken
  poller from a genuinely idle panel - they looked identical before this).
  Runs 24/7 across all four sources including the Home Grid Meter, and
  sends a recovery notice once polling resumes.
- **Automated test suite** - 48 tests covering the logic that's actually
  bitten this project: the day-boundary aggregation bug, the TNEB tariff
  calculation (regression-tested against real validated bills), forecast
  deviation math, and every alert state machine (fault/battery/source-down/
  push robustness), each formalizing a scenario that was previously only
  checked by hand. `pytest` from the project root; uses its own throwaway
  DB, never touches the real one.

## [1.3.1] - 2026-07-11

Replaced the dashboard's "Current TNEB bill estimate" card with a simple
link to `/bill` (per feedback right after 1.3.0 shipped - the detail
belongs on the dedicated bill page, not the main dashboard). The `/bill`
page itself is unchanged, including the savings history added in 1.3.0.
Also removed the now-unused `bill-calc.js` include and its cache-busting
on the dashboard route, since only `/bill` needs it now.

## [1.3.0] - 2026-07-11

Completes the "Financial" item from the earlier improvements list:

- **Live bill estimate on the main dashboard** - a new card under the Home
  section shows the current cycle's running bill (net units, tier, total)
  without needing to visit `/bill` or enter any dates manually.
- **Savings vs. no-solar hypothetical** - alongside the live estimate,
  shows what the same usage would have cost with no solar at all, and the
  difference. Computed from real energy conservation (solar generated -
  exported + imported - net battery charge = total home consumption),
  billed via the same validated tariff slabs but without net-metering
  credit or the wheeling charge (neither would exist without solar).
- **Savings history** on `/bill` - cumulative savings since Home Grid
  Meter tracking began, broken down by month. Will fill in and become
  more meaningful as more history accumulates (currently just days old).
- `/bill` now persists its "last bill date" server-side (previously reset
  to a hardcoded default on every page load) - update it whenever a new
  real bill arrives via a new admin-gated control, and the dashboard's
  live estimate automatically uses it as the reference point.

Refactored the bill math out of `bill.js` into a shared `bill-calc.js`
(used by both `/bill` and the dashboard) and ported the same validated
formula to Python (`app/tneb_bill.py`, cross-checked to produce identical
results) for the server-side savings calculations, which need to combine
Home Grid Meter, Deye, and Growatt Home data in ways that don't fit
cleanly in client-side JS.

## [1.2.1] - 2026-07-11

Fixed the admin password check (`/api/subscribers/list` and friends)
throwing a 500 error instead of a clean "incorrect password" whenever the
entered password contained a non-ASCII character (e.g. a smart-quote or
accented character from a phone keyboard) - `secrets.compare_digest` only
accepts ASCII when comparing `str`, but works with any content as `bytes`.
Found live in production logs while verifying the 1.2.0 deploy (unrelated
to that release - a real user hit it live while I was checking).

## [1.2.0] - 2026-07-11

Two new alert types, both sent via email and push (completing the
"Alerting" item from the earlier improvements list - non-email channels
were already covered by push notifications in 1.1.0):

- **Safety fault alerts** - the Home Grid Meter reports a `fault` status
  bitmap (short circuit, overload, fire alarm, and 17 other conditions)
  that was being polled every cycle but never decoded or surfaced
  anywhere - a real monitoring gap for a home electrical system. Now
  decoded and alerted on immediately (no delay/threshold, unlike
  zero-output) whenever a new fault becomes active. Alerts once per
  continuous occurrence - clears when the fault clears, re-alerts if it
  recurs, and multiple simultaneous faults are tracked independently.
- **Low battery alerts** - fires once per day if the Deye battery's SOC
  drops to `BATTERY_LOW_SOC_ALERT_PCT` (default 25%, comfortably above
  the 18% auto-shutdown threshold), giving advance warning before backup
  power is lost.

Both were tested with a full state-machine simulation (new/persisting/
cleared/recurring faults, threshold crossing, same-day dedup, missing
data) before deploying, given the earlier fault-boundary and exception-
handling bugs found in this session.

## [1.1.1] - 2026-07-11

Fixed an extra Tuya API call on every service restart: the scheduler was
set to fire `poll_home_meter` immediately on startup (`next_run_time=now()`),
so every deploy or restart triggered one out-of-cycle Tuya poll on top of
the normal 300s interval. Harmless in steady-state operation, but adds up
during active development with frequent deploys. Removed the immediate
first run for this job specifically - `poll_once` (Deye/Growatt) still
fires immediately on restart since it's unaffected by the Tuya quota.

## [1.1.0] - 2026-07-11

Added three UX features:

- **CSV export** (`/api/export/csv`) - download daily totals for any source
  and date range, with a picker on the dashboard.
- **Year-over-year comparison chart** - monthly totals per source, one line
  per year, to spot seasonal patterns or gradual panel degradation. Backed
  by the existing `/api/daily` endpoint, no new backend query needed.
- **PWA installability** - `manifest.json`, a service worker (`/sw.js`), and
  icons so the dashboard can be installed as an app on desktop/mobile.
- **Push notifications** - opt-in browser/phone push for zero-output alerts,
  alongside email. New `PushSubscription` table, VAPID-based Web Push via
  `pywebpush`, wired into the existing zero-output alert trigger.

Two bugs caught and fixed during testing, before they ever reached
production:
- The service worker's initial caching strategy cached `/` itself (the
  server-rendered HTML) - would have made every future deploy invisible to
  installed users until they force-refreshed. Fixed to network-first for
  the HTML document, cache-first only for true static assets.
- `push_notify.send_to_all()` only caught `WebPushException` - a malformed
  or corrupted subscription would raise a different exception type and
  silently abort the whole batch, blocking notifications to every other
  subscriber. Broadened to catch any per-subscription failure and continue.

## [1.0.2] - 2026-07-11

Clarified the forecast deviation display, which was showing a deviation
figure (e.g. "-1.2 kWh below forecast") without the reference point it was
computed against, making the math look inconsistent with the full-day
forecast total shown above it (deviation is actual-so-far vs.
forecast-so-far *by this point in the day*, not vs. the full-day total).
Added `forecast_so_far_kwh` to `/api/forecast` and updated the dashboard
to show "expected X kWh by now, actual Y kWh" alongside the deviation.

## [1.0.1] - 2026-07-11

Fixed a quota-exhaustion risk: the Home Grid Meter (Tuya) was polling every
10 seconds, each poll costing one Tuya Cloud API call against a 30,000/month
quota. At that rate, the remaining monthly budget (~10,874 calls) would have
been exhausted in under 4 days, breaking Home Grid Meter data collection for
the rest of the billing cycle. Slowed `HOME_METER_POLL_INTERVAL_SECONDS` to
300 (5 minutes, matching the other sources' cadence) — ~288 calls/day, ~38
days of runway on the remaining budget. Updated the About page's stated
refresh rate to match.

## [1.0.0] - 2026-07-10

Initial versioned baseline. Established as the reference point going
forward — everything before this tag was pre-versioning development;
everything after gets its own entry here.

Feature set at this baseline:

- Live monitoring dashboard combining four sources: Deye hybrid inverter,
  Growatt Home, Growatt Farm, and the Tuya Home Grid Meter (per-phase
  voltage/current/power, power factor).
- History section with per-day power curves, and a readable summary
  fallback for backfilled days with only a daily total.
- Energy statistics (day/month/year) per source, with per-metric toggles.
- Solar generation forecast: a 7am weather-based morning prediction per
  array (Deye, Growatt Home, Growatt Farm) using real panel specs and a
  PVWatts-style irradiance model, self-correcting through the day against
  actual output, with sunrise/sunset times, day length, and an
  end-of-day deviation readout.
- TNEB/TANGEDCO bill calculator (`/bill`) for the LA1A tariff with solar
  net metering — formula reverse-engineered and validated exactly against
  real bills, with an editable date-range loader that sums actual Home
  Grid Meter data for any billing period.
- Email subscriptions: daily summary, periodic status updates, and
  zero-output alerts, admin-gated.
- `/about` page documenting data sources and dashboard sections.
- Security hardening: WAL-mode SQLite (eliminated "database is locked"
  errors), constant-time admin password comparison, no plaintext secrets
  in logs, HestiaCP webshell fixed and origin-restricted, firewall
  verified to block all internal-only ports from the internet.
- Fixed a shared day-boundary aggregation bug where a stale reading
  straddling local midnight could leak the previous day's total into
  today's hourly/daily energy stats (affected `daily_energy_totals`,
  `daily_deye_breakdown`, and `home_meter_daily_breakdown`).