• v1.5.0 b405aea88b

    v1.5.0 — prices chart: fix hover past midnight, mark the active price
    frontend / frontend (push) Successful in 9m59s
    pytest / test (push) Successful in 11m51s
    docker-image / build-and-push (push) Successful in 12m49s
    Stable

    tliu93 released this 2026-07-27 20:31:13 +02:00 | 0 commits to main since this release

    Fixes

    • prices: the chart keyed its X axis on "HH:mm" labels, which repeat across a
      today+tomorrow range. Recharts resolves axis tooltips by value, so hovering a
      slot after midnight matched today's identically labelled point — the tooltip
      showed today's prices and the marker jumped back to today's position. The axis
      is now keyed on the ISO instant; ticks still read HH:mm and the tooltip label
      carries the date.

    Features

    • prices: the price slot currently in effect is marked by default with a dot on
      the buy and sell lines, plus a caption with its prices; re-evaluated every 30s.

    Chores

    • frontend: regenerate src/api/schema.d.ts, which had drifted from
      openapi/openapi.json since d07a083 and was failing CI's codegen check.
    • docs: the validation gates now spell out that a route/schema/docstring change
      needs both scripts/export_openapi.py and npm run codegen committed.
    Downloads
  • v1.4.1 f4cea3874b

    v1.4.1 — Tibber price fetch fix (fetch newest slots)
    frontend / frontend (push) Failing after 6m38s
    pytest / test (push) Successful in 20m16s
    docker-image / build-and-push (push) Successful in 13m32s
    Stable

    tliu93 released this 2026-07-17 18:53:20 +02:00 | 5 commits to main since this release

    Bug fix release.

    • fix(tibber): priceInfoRange now uses last:192 instead of first:96, so the
      hourly refresh fetches the newest quarter-hourly slots (today + tomorrow)
      instead of being stuck on the oldest slots anchored at the subscription start
      date. Fixes the empty "No Tibber price points available" state on the energy
      prices page for contracts added mid-period.
    • test(energy-cost): pin local_now in the future-window summarize test so it is
      deterministic regardless of the wall-clock date.
    Downloads
  • v1.4.0 3e04b15656

    v1.4.0 — DDSU666 Modbus meter support
    frontend / frontend (push) Successful in 2m21s
    pytest / test (push) Successful in 9m50s
    docker-image / build-and-push (push) Successful in 4m24s
    Stable

    tliu93 released this 2026-06-30 17:20:55 +02:00 | 7 commits to main since this release

    v1.4.0 — DDSU666 Modbus meter support

    Adds support for the CHINT DDSU666 single-phase smart meter alongside the
    existing Eastron SDM120, and generalizes the Modbus read driver so the read
    function code is chosen per device profile instead of being hardcoded.
    Validated end-to-end against real DDSU666 hardware.

    Highlights

    • 🆕 DDSU666 support — a second supported energy meter model.
    • 🔧 Per-profile read function code — the driver now selects FC03 (holding) vs FC04 (input) registers based on the device profile.
    • Hardware-validated — confirmed on a live DDSU666 (big-endian word order, FC03 reads, energy accumulation).

    Added

    • DDSU666 device profile (ddsu666.yaml): reads via FC03 holding registers — voltage, current, active power (kW), reactive power (kvar), power factor, frequency, import/export active energy. Big-endian word and byte order.
    • DDSU666 Modbus protocol reference (docs/references/DDSU666-Modbus-Protocol.md), extracted from the official CHINT manual: register map, frame format, function codes, CRC/exception handling, an SDM120 comparison table, and field notes from real-hardware validation.

    Changed

    • driver.read_blocks now selects the read function code per profile (function_code: 4 = FC04 input registers — default, SDM120; 3 = FC03 holding registers — DDSU666), validated before connecting. Previously the read path was hardcoded to FC04 and the profile's function_code field was declared but never honored.
    • function_code is now threaded through every read path: the modbus_cli read command, the background poller, and the device /test endpoint. The CLI read output additionally prints the active function code.

    Compatibility

    • SDM120 behavior is unchanged (still FC04).
    • No API or OpenAPI schema changes.
    • Read-only — no meter configuration registers are ever written.

    Verification

    • Full test suite green (1037 passed), ruff clean, OpenAPI unchanged; multi-stage Docker image builds, migrates, and boots cleanly.
    • Confirmed on a live DDSU666: big-endian word order correct, FC03 reads decode to sensible values, cumulative energy (kWh) accumulates correctly.
    • Note: at load currents below the meter's measuring range (Imin ≈ 0.25 A), the instantaneous active power and power factor read 0 while energy still accumulates — expected meter behavior at very low current, not a decoding issue.

    Docs

    • Roadmap updated to queue Authentication next-steps (sliding session renewal + long-lived token targets).
    Downloads
  • v1.3.0 90a03e7fd6

    v1.3.0: Meter epochs (M7) + energy-cost HA integration & midnight settlement fixes
    docker-image / build-and-push (push) Successful in 4m29s
    frontend / frontend (push) Successful in 2m17s
    pytest / test (push) Successful in 11m2s
    Stable

    tliu93 released this 2026-06-27 11:18:24 +02:00 | 9 commits to main since this release

    v1.3.0

    A large release covering everything since v1.2.0 — it takes the backend from
    single-user auth hardening through a full IoT + energy-metering stack with deep
    Home Assistant integration.

    Highlights

    • Home Assistant integration via MQTT Discovery — Modbus devices and energy-cost sensors auto-appear in HA.
    • Energy metering & cost engine — DSMR smart-meter ingest, manual/dynamic pricing (incl. Tibber), per-15-minute cost billing with standing charges and tax credits.
    • Meter epochs (meter-swap archival) — swapping a physical meter spins up a brand-new HA sensor and never mixes readings across meters.
    • Login hardening — TOTP two-factor auth plus exponential login back-off.

    Authentication & security (M4)

    • Two-factor authentication (TOTP): QR setup, recovery codes, and a two-step login flow.
    • Exponential back-off throttling on failed logins.
    • admin_cli escape hatches: reset password, unlock account, disable/reissue TOTP, list admins.

    IoT devices & Home Assistant (M5)

    • Generic Modbus TCP polling: device CRUD, JSON readings, YAML device profiles (e.g. SDM120), opt-in scheduled polling, cascade delete.
    • MQTT client with lifespan connect / auto-reconnect and a config "test" button.
    • Home Assistant MQTT Discovery: an entity-exposure framework with per-entity enable/disable toggles; publishes discovery configs, state, and availability under the home_automation topic prefix.
    • UI: sidebar navigation, Energy/device management views, latest-reading cards + trend charts, accordion config, and a Home Assistant expose panel.
    • DB-level foreign-key enforcement (SQLite foreign_keys pragma).

    Energy pricing, DSMR & cost engine (M6)

    • Pluggable pricing layer with manual and Tibber strategies plus profile validation.
    • Energy contracts with versioned tariffs.
    • DSMR P1 telegram ingest over MQTT.
    • Billing engine: per-15-minute period cost computation, dual-tariff buy/sell prices, standing fees folded into import cost and the energy-tax credit into export revenue; summary + retroactive recompute.
    • Tibber GraphQL client with hourly price refresh (today + tomorrow).
    • Energy API (prices / costs / summary / latest DSMR / recompute / Tibber test) plus frontend contract management, price/cost views, and a DSMR data panel.

    Meter epochs / meter-swap archival (M7)

    • New meter table modelling each physical meter's install window [started_at, ended_at); cost periods now carry a meter_id.
    • Meter-aware billing: never computes a register delta across two meters; periods straddling a swap boundary are marked degraded; added a delta sanity guard.
    • Cumulative cost sensors are anchored to the active meter's start, so a swap cleanly resets the running totals for the new meter.
    • Meter CRUD API with retroactive recompute, plus a meter-management UI (timeline, declare swap, edit).
    • Each meter has a stable UUID used as its Home Assistant identity, and the energy-cost HA device is named after the meter label — so swapping a meter produces a brand-new HA device/sensor and never contaminates old data. Discovery is re-published automatically on meter changes.

    Energy time & Home Assistant midnight handling (follow-ups)

    • All energy time math localized to the server timezone; the UI shows energy/DSMR/meter times in local time (24h).
    • Fixed fees and the energy-tax credit accrue per local calendar day; any day with recording counts its full-day credit (symmetric start/end-day handling).
    • Daily-resetting "today" cost/revenue sensors in addition to lifetime totals.
    • Midnight handling tuned for Home Assistant statistics: the daily "today" reset is shifted to just past local midnight (robust to a few seconds of clock skew between hosts), and each day's fixed fee + tax credit are settled at ~01:05 local — off the midnight hour-boundary — so HA attributes them to the correct day and the energy dashboard's daily costs stay accurate.
    • Billing robustness: ignore stale / out-of-range DSMR readings; energy-cost sensors marked always-available.

    Upgrade notes

    • Database migrations run automatically. The new meter table is created and backfilled — existing electricity history is attributed to an initial meter with a generated UUID. No data is deleted.
    • After upgrading, declare your meter (set its label) so the Home Assistant energy-cost device is named correctly. The old generic "energy-cost" HA device is superseded by a per-meter device; you may delete the stale one in HA.
    • (Optional) To include standing charges and the tax credit in the Home Assistant Energy dashboard, set the grid cost source to "Use an entity tracking the total costs" pointing at import_cost_total (and export_revenue_total for return-to-grid).
    Downloads
  • v1.2.0 1a3aaea933

    v1.2.0
    frontend / frontend (push) Successful in 1m19s
    pytest / test (push) Successful in 1m31s
    docker-image / build-and-push (push) Successful in 3m55s
    Stable

    tliu93 released this 2026-06-13 17:21:26 +02:00 | 88 commits to main since this release

    Replaces the server-side Jinja pages with a same-origin React single-page app,
    backed by a new JSON API. Same container, same origin (no CORS); auth still
    rides the existing Argon2 + server-side session cookie.

    Added

    • React SPA (Vite + TypeScript + Mantine + TanStack Query + React Router),
      served same-origin from FastAPI as static build output.
    • JSON API for the SPA:
      • GET/PUT /api/config — config read/write
      • POST /api/config/smtp/test — SMTP test action
      • read-only data query endpoints (location / poo)
      • single-record CRUD (PATCH / DELETE)
      • session/auth endpoints (/api/session, login, logout, forced password change)
    • Data visualization: Leaflet heatmap home page (viewport-normalized
      intensity, dark basemap) + records-management UI (paginated lists, edit/delete
      modals) — takes over what Grafana used to do.
    • Typed OpenAPI client generated from scripts/export_openapi.py output.
    • Multi-stage Dockerfile (node build → python runtime) + frontend CI workflow.
    • Explicit docker-compose.dev.yml dev stack (port 8001, -dev container
      names, prod-copy ./data DB).
    • Dark-mode toggle, Feather-icon nav, Grafana-style quick time-range presets.

    Changed

    • Browser endpoints reuse the existing session-cookie auth with presence-only
      CSRF (X-CSRF-Token).
    • README + architecture-overview updated for the SPA reality; retire the
      "no frontend/backend split" and "Grafana as visualization" constraints.

    Removed

    • Jinja templates (base/config/home/login.html) and the old auth.py /
      pages.py page routers.
    • docker-compose.override.yml (renamed to the explicit docker-compose.dev.yml).

    Fixed

    • Logout no longer floods GET /api/session with 401s — the session probe and
      the login endpoint own their 401s, breaking the redirect→refetch loop.
    • Heat-layer white-screen crash on first load after login.

    Notes

    • The device ingestion endpoint stays on bare API until M3 (token auth).
    • TOTP 2FA for the now public-facing dashboard is recorded under
      Future Ideas — not scheduled.
    Downloads
  • v1.1.0 c1a5d7a425

    v1.1.0 — Single-database consolidation (M1)
    pytest / test (push) Successful in 50s
    docker-image / build-and-push (push) Successful in 3m52s
    Stable

    tliu93 released this 2026-06-12 20:53:09 +02:00 | 120 commits to main since this release

    Consolidates the project's three separate SQLite databases (app / location / poo) into a single app.db: one SQLAlchemy Base, one engine, one Alembic chain. Cleans up the data-access code that had accumulated across the codebase and removes Grafana. No historical data is lost — the location/poo rows are moved by a one-time, idempotent, reconciled migration script.

    ▎ First step of the post-v1.0.3 roadmap (M1 → M2 frontend → M3 mobile): lay a clean foundation first. This is a structural change with a breaking configuration change and a mandatory one-time data migration after deploy — read the Upgrade Guide before rolling out.

    Highlights

    • Single database. location and poo_records are merged into app.db. The data layer is unified onto one Base + a cached engine bound to app_database_url with SQLite WAL, behind a single get_db. app/auth_db.py, app/poo_db.py, app/models/base.py removed.
    • One Alembic chain. A new alembic_app revision creates location / poo_records (column types match the legacy schema exactly — REAL/TEXT, nullability, PK order). The alembic_location / alembic_poo chains and adoption scripts are deleted.
    • One-time migration script scripts/migrate_legacy_data.py: ATTACH + INSERT OR IGNORE (idempotent), explicit columns, full-row NULL-safe reconciliation that aborts non-zero on any missing/differing row. Never deletes or overwrites files; not in the Alembic chain or startup path.
    • Config cleanup (⚠️ breaking). Settings drops location_database_url / poo_database_url and their *_sqlite_path; only APP_DATABASE_URL remains. Config page and .env.example updated.
    • Grafana removed. Service, the homeautomation_grafana_storage volume declaration, and the grafana/ directory deleted. Visualization moves to the M2 React frontend (intentional interim gap).
    • Docs & OpenAPI. README / architecture-overview / roadmap updated to single-DB; openapi/ re-exported (also catching up /config/smtp/test, /public-ip/check, PublicIPCheckResponse).

    ⚠️ Upgrade guide (required after deploy, in order)

    The new code only opens app.db at runtime; deploying does not move historical data by itself — it only creates the empty tables.

    1. Back up production app.db, locationRecorder.db, pooRecorder.db.
    2. Edit .env: remove LOCATION_DATABASE_URL / POO_DATABASE_URL (now ignored); keep APP_DATABASE_URL.
    3. Deploy the new image → the migration job (python -m scripts.run_migrations) upgrades app.db and creates the empty location / poo_records.
    4. Run the one-time data migration (otherwise historical location/poo data is absent):
      python -m scripts.migrate_legacy_data
      --app-db sqlite:///./data/app.db
      --location-db sqlite:///./data/locationRecorder.db
      --poo-db sqlite:///./data/pooRecorder.db

    exit code must be 0; add --dry-run first to preview

    1. Prefer a brief quiet/maintenance window (SQLite single-writer; avoids a new write colliding with a historical PK and tripping reconciliation).
    2. Verify: app.db location / poo_records row counts equal the legacy DB counts.
    3. Retire the old DBs (manual, after verification, keep an archive): archive the legacy files (don't delete blindly); decommission the Grafana container and the homeautomation_grafana_storage volume.

    ▎ No automated path ever deletes the old files — retiring them is always a manual, reversible step.

    🔒 Data safety

    The migration reads the legacy DBs read-only (no os.remove/unlink/shutil/truncate/DROP/DELETE); idempotency comes from INSERT OR IGNORE. Verified end-to-end against copies of the real production databases: dry-run wrote nothing, the real run migrated every row with matching per-row checksums, a second run was idempotent, a constraint-violating row aborted reconciliation non-zero, and the legacy files were md5-identical throughout.

    🧹 Internal cleanup

    Startup converged (lifespan validates only the app DB, still fail-closed; run_migrations returns {"app": ...}); routes/deps/models on the single data layer; test suite converged (legacy adoption tests removed, migration-script tests added); pytest green (97 passed); openapi/ re-exported.

    Changelog

    PR #7 (feature/m1-db-consolidation → main, merge 1e0b235): T01 table revision · T02 migration script · T03 unified data layer (WAL) · T04 startup convergence · T05 config cleanup · T06 remove Grafana · T07 docs + OpenAPI · review rework (full-row reconciliation). Full diff: v1.0.3...v1.1.0

    Downloads
  • v1.0.3 636bb2b80b

    v1.0.3
    docker-image / build-and-push (push) Successful in 3m59s
    pytest / test (push) Successful in 53s
    Stable

    tliu93 released this 2026-04-29 13:18:03 +02:00 | 132 commits to main since this release

    This release adds Grafana provisioning, public IPv4 monitoring, SMTP support, and automatic email notifications for public IP changes.

    Added

    • Grafana provisioning
    • Public IPv4 detection and persistence
    • Public IP state/history storage in the app DB
    • Manual endpoint to trigger a public IP check
    • Scheduled public IP checks
    • SMTP configuration and test email support
    • Automatic email notifications when the public IP changes

    Changed

    • Refined sender display for notification emails

    Notes

    • Public IP change notification emails are sent in English
    • Notifications are only sent when an actual IP change is detected
    • SMTP settings can be configured and persisted from the config page

    Downloads
  • v1.0.2 8565534b73

    v1.0.2
    pytest / test (push) Successful in 45s
    docker-image / build-and-push (push) Successful in 3m40s
    Stable

    tliu93 released this 2026-04-22 13:35:57 +02:00 | 138 commits to main since this release

    Downloads
  • v1.0.1 c9af7530e5

    Release v1.0.1 changes compose behavior
    pytest / test (push) Failing after 44s
    docker-image / build-and-push (push) Successful in 3m40s
    Stable

    tliu93 released this 2026-04-22 13:28:51 +02:00 | 140 commits to main since this release

    Downloads
  • v1.0.0 35aee79d93

    1.0.0
    pytest / test (push) Successful in 43s
    docker-image / build-and-push (push) Successful in 3m38s
    Stable

    tliu93 released this 2026-04-20 23:34:44 +02:00 | 142 commits to main since this release

    Downloads