From 279a1f3f3e639baf501c7180ab875988cd6c2e96 Mon Sep 17 00:00:00 2001 From: Darek Date: Mon, 13 Apr 2026 20:12:49 -0500 Subject: [PATCH] Update README and CLAUDE.md for Home Assistant integration --- CLAUDE.md | 10 ++++++++-- README.md | 47 +++++++++++++++++++++++++++++++++++------------ 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 70be39b..bb76d6d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,8 +40,8 @@ journalctl --user -u battery-tracker -f `config.py` reads `DATABASE_URL` from the environment, falling back to `sqlite:///batteries.db`. Swapping to MariaDB is entirely a matter of setting that env var — no code changes needed. All column types are restricted to `Integer`, `String`, `Text`, `Boolean`, `DateTime` for MariaDB compatibility. Status is stored as `String(20)` (not `Enum`) to avoid DDL differences between SQLite and MariaDB. ### Models -- `Battery`: label (unique), brand, status (`available`/`installed`/`retired`), device_id (FK nullable, `ondelete=SET NULL`), notes, size, chemistry, capacity_mah, tested_capacity_mah, tested_date, charge_cycles, purchase_date, storage_location -- `Device`: name (unique), battery_slots, device_type, notes +- `Battery`: label (unique), brand, status (`available`/`installed`/`retired`), device_id (FK nullable, `ondelete=SET NULL`), notes, size, chemistry, capacity_mah, tested_capacity_mah, tested_date, charge_cycles, purchase_date, storage_location, battery_percentage +- `Device`: name (unique), battery_slots, device_type, notes, ha_entity_id - Helper methods: `Battery.is_available/installed/retired()`, `Device.installed_count()`, `Device.installed_brands()`, `Device.has_mixed_brands()` ### Business rules (enforced in routes, not DB constraints) @@ -52,6 +52,10 @@ journalctl --user -u battery-tracker -f - Bulk install into device: capacity check before write; batteries already in target device are skipped; retired batteries are skipped - Unretire: sets status back to `available` - Unassign with `next` form field → redirects to that URL (must start with `/`); falls back to dashboard +- Logging a charge entry → sets `battery.battery_percentage = 100` + +### Home Assistant integration (optional) +`ha_client.py` wraps the HA REST API (`GET /api/states/`). `ha_poller.py` runs a daemon thread started in `create_app` only when `HOMEASSISTANT_URL` and `HOMEASSISTANT_API_KEY` are set. The poller queries all `Device` rows with `ha_entity_id IS NOT NULL`, fetches the current percentage from HA, and writes it to `battery_percentage` on each installed battery in that device. The poller uses its own `sessionmaker` session (not the request-scoped `scoped_session`). When HA is not configured the app behaves exactly as before — all HA UI is gated on `ha_enabled` passed to templates. ### Adding new columns to existing DB `create_all()` won't add columns to existing tables. Run via Python: @@ -66,5 +70,7 @@ Always snapshot the DB first: `cp batteries.db batteries.db.$(date +%Y-%m-%d).sn ### Testing Tests use a temporary file-based SQLite DB (via `tempfile.mkstemp`) created fresh per test — avoids SQLite in-memory per-connection isolation issues. The `seeded_client` fixture in `conftest.py` pre-populates via HTTP POST calls (not direct DB access), so tests exercise the full stack. Battery IDs in tests are positional (id=1 is always the first battery POSTed by the fixture). +`TestConfig` sets `HOMEASSISTANT_URL = None` to prevent the HA poller thread from starting during tests. HA integration tests live in `tests/test_ha_integration.py` and use a separate `ha_app` fixture with a fake HA URL; they call `poller._poll_once()` directly rather than waiting for the background thread to fire. HA API calls are mocked with `unittest.mock.patch("ha_client.requests.get")`. + ### MariaDB migration `migrate_to_mariadb.py` opens two SQLAlchemy sessions simultaneously (SQLite source, MariaDB destination), migrates Devices first (FK dependency), then Batteries with explicit `id=` values to preserve FK links, then resets `AUTO_INCREMENT` via `text("ALTER TABLE ...")`. Takes `MARIADB_URL` from env or CLI arg. See `MIGRATION.md` for the full procedure. diff --git a/README.md b/README.md index f566f4c..968b58a 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Built with Flask + SQLAlchemy. Runs on a Raspberry Pi or any Linux box with a sy - Track status: **Available**, **Installed**, **Retired** - Store metadata: size (AA/AAA/18650/…), chemistry (NiMH/Li-ion/…), capacity, tested capacity, charge cycles, purchase date, storage location, notes - Visual health indicator when tested capacity is recorded (good/warn/bad) +- Battery percentage field — set manually or synced from Home Assistant; resets to 100% when a charge is logged - Retire and unretire batteries ### Devices @@ -18,15 +19,22 @@ Built with Flask + SQLAlchemy. Runs on a Raspberry Pi or any Linux box with a sy - Install batteries into devices — by brand/quantity or by picking a specific battery - Bulk install multiple batteries at once from the dashboard - Mixed-brand warning when batteries from different brands share a device +- Link a device to a Home Assistant battery sensor to automatically track charge level - Edit device details in-place ### Dashboard -- Overview stats (total / available / installed / retired) +- Overview stats (total / available / installed / retired) + **Low Battery** count when any battery is below 20% - Full battery table with client-side filtering by status, brand, size, storage location, and free-text search -- Column picker (Chemistry, Capacity, Storage, Purchase Date, Cycles) stored in localStorage +- Column picker (Chemistry, Capacity, Storage, Purchase Date, Cycles, Battery %) stored in localStorage - Quick-assign available batteries to a device without leaving the page - Bulk actions: Unassign, Retire, Delete, Set Field (storage location or brand), Install in Device +### Home Assistant Integration (optional) +- Link any device to a Home Assistant entity (e.g. `sensor.tv_remote_battery`) +- A background poller fetches the current percentage on a configurable interval (default 5 min) and updates all installed batteries in that device +- Low-battery warning badge (< 20%) on the dashboard, device detail, and battery detail pages +- Fully optional — when `HOMEASSISTANT_URL` is not set the app works exactly as before + ### UI - Mobile-friendly: card-style table rows on small screens, full-width filter controls, stacked form buttons - Dark mode — follows the OS system preference automatically (`prefers-color-scheme`) @@ -43,7 +51,7 @@ Built with Flask + SQLAlchemy. Runs on a Raspberry Pi or any Linux box with a sy | Database | SQLite (dev) / MariaDB (prod) | | WSGI server | Waitress | | Process manager | systemd user service | -| Tests | pytest (45 acceptance tests) | +| Tests | pytest (71 acceptance tests) | --- @@ -75,7 +83,7 @@ flask run ### Run (production — systemd user service) ```bash -bash sbin/install-service.sh # prompts for host/port, installs and enables the service +bash sbin/install-service.sh # prompts for host/port + optional HA config, installs and enables the service systemctl --user start battery-tracker systemctl --user status battery-tracker journalctl --user -u battery-tracker -f @@ -89,7 +97,19 @@ SQLite is used by default (`batteries.db` in the project root). To switch to Mar export DATABASE_URL="mysql+pymysql://user:pass@host/dbname" ``` -See `MIGRATION.md` for migrating an existing SQLite database to MariaDB. +See `MIGRATION.md` for migrating an existing SQLite database to MariaDB, and for the schema migration commands needed when upgrading an existing database. + +### Home Assistant integration + +Set these environment variables before starting (or enter them when running `install-service.sh`): + +```bash +export HOMEASSISTANT_URL="http://homeassistant.local:8123" +export HOMEASSISTANT_API_KEY="" +export HOMEASSISTANT_POLL_INTERVAL="300" # seconds, default 300 +``` + +Then link a device to its HA sensor entity ID on the device's edit page (e.g. `sensor.tv_remote_battery`). The app will poll HA every interval and update the battery percentage for all installed batteries in that device. ### Seed data (optional) @@ -116,24 +136,27 @@ Tests use a fresh temporary SQLite database per test and exercise the full HTTP ``` app.py Flask app factory + all routes models.py SQLAlchemy models (Battery, Device) -config.py Database URL config +config.py Database URL + HA config +ha_client.py Home Assistant REST API wrapper +ha_poller.py Background thread — polls HA, updates battery_percentage seed.py Optional seed data script migrate_to_mariadb.py SQLite → MariaDB migration script -MIGRATION.md MariaDB migration procedure +MIGRATION.md Schema migration instructions + MariaDB procedure requirements.txt sbin/ - install-service.sh systemd user service installer + install-service.sh systemd user service installer (prompts for HA config) templates/ base.html Layout, CSS variables, dark mode, nav - dashboard.html Battery list with filters and bulk actions + dashboard.html Battery list with filters, bulk actions, HA% column battery_add.html Add batteries form - battery_detail.html Battery detail + edit + battery_detail.html Battery detail + edit (includes battery_percentage field) battery_delete.html Delete confirmation assign.html Assign battery to device device_list.html Device list with type filter device_add.html Add device form - device_detail.html Device detail + install + edit + device_detail.html Device detail + install + edit (includes ha_entity_id field) tests/ conftest.py pytest fixtures - test_acceptance.py 45 acceptance tests + test_acceptance.py 50 acceptance tests + test_ha_integration.py 21 HA integration tests ```