Architecture
Home Keeper Library is a Home Assistant integration for the books of a household. It records the rooms, bookcases and shelves of the home, the books and their copies, the reading status of each person, the loans and a wishlist. It is a companion of Home Keeper: admins manage the library in a tab of the Home Keeper panel. Every user reads and updates their own reading status through a card, native entities and services.
Goals
- G1. Testable core. The rules for records, ISBNs, Open Library replies, CSV files, projections and the sync plans run without Home Assistant, so the fast unit tier and the mutation gate can check them.
- G2. One write path. Every change goes through 1 store method. That method checks the input, saves the change and fires its events (store-models).
- G3. Services first. Each data action is a
home_keeper_library.*service, so automations, scripts, voice and other integrations can do what the tab does. - G4. Per-person privacy. A non-admin user changes only their own reading data, and reads only what the projections allow (people-privilege).
- G5. Useful without the tab. If Home Keeper is missing or too old, the store, the services, the card and the entities still work.
Non-goals
- A sidebar panel of its own. The admin UI is a tab in the Home Keeper panel (home-keeper-dependency).
- More than 1 config entry. The manifest sets
single_config_entry. - A book catalog service other than Open Library, or a sync between homes. The only cloud account is the Open Library account that sends books.
Design
Module map
| Layer | Modules | Rule |
|---|---|---|
| Pure core | const.py, isbn.py, models.py, events.py, projections.py, openlibrary.py, openlibrary_contribute.py, csv_io.py, import_export.py, wishlist.py, loan_tasks.py, card_resource.py, backend_i18n.py, api_surface.py | No homeassistant import. Time comes in as an argument. |
| Boundary | store.py, coordinator.py, people.py, openlibrary_client.py, openlibrary_account.py | Talks to Home Assistant storage, the bus, the person registry and HTTP. |
| Glue | __init__.py, services.py, websocket_api.py, covers.py, book_lookup.py, home_keeper.py, loan_sync.py, wishlist_sync.py, frontend_assets.py, card.py, config_flow.py, diagnostics.py | Registers surfaces and runs the syncs. |
| Platforms | sensor.py, todo.py, with entity.py | const.PLATFORMS lists them. |
The mutation allowlist (only_mutate in pyproject.toml) holds the pure modules with
logic. api_surface.py declares each surface that an integrator sees
(events-api).
Setup
async_setup does nothing. CONFIG_SCHEMA is cv.config_entry_only_config_schema.
The manifest has lovelace in after_dependencies, because the card writes a Lovelace
resource.
async_setup_entry runs these steps in order:
- Read the string tables of the Home Assistant language in an executor job
(
backend_i18n.preload), and load the store. - Make
LibraryCoordinatorand give it the Open Library client, the lookup queue and the 2 syncs. Store it asentry.runtime_data. A store listener sends the dispatcher signalconst.SIGNAL_STORE_CHANGEDfor the websocket subscriptions. An update listener of the entry firessettings_updatedwhen the currency or the account changes. - Register the static path, the card delivery, the websocket commands and the cover views (frontend-tab-card).
- Forward
const.PLATFORMSand register the services. - Start the lookup queue, and queue again each book that still needs a lookup (open-library-covers). Start the wishlist sync and the loan sync.
- Start
home_keeper.HomeKeeperLink, which adds the tab and the companion or shows a repair issue. - Fire the overdue loans, start the hourly overdue check, and delete the old uploads that no book uses.
Unload
async_unload_entry unloads the platforms and removes each service in
api_surface.SERVICE_NAMES. Each sync, the queue and the Home Keeper link stop through
entry.async_on_unload. The link removes the tab. The static path and the card resource
stay, because most unloads are half of a reload. async_remove_entry deletes the card
resource when the integration is removed.
Config entry
The config flow checks Home Keeper first and aborts with the reason if the check fails.
Then it asks for the currency, with the currency of Home Assistant as the default. The
options flow is a menu. currency returns config_flow.merge_flow_input, so no stored
key is lost. openlibrary keeps the keys of an Open Library account in the entry data.
Localized text
The pure core raises models.LibraryError with a key of strings.json exceptions. A
service turns it into a ServiceValidationError, which Home Assistant translates. A
websocket error and an HTTP view error show their text as is, so
backend_i18n.resolve_exception resolves it in the language of Home Assistant. Text that
is not an exception, such as the tab title and the loan task names, is in
backend_strings/<lang>.json and resolves with backend_i18n.resolve_string.
Diagnostics
diagnostics.async_get_config_entry_diagnostics returns the options, the tab state, the
revision, the counts and the library document. It redacts the names of loan parties, the
notes and acquired_from, and it has no Open Library key.
Design doc index
| Doc | Subject |
|---|---|
| store-models | The storage document, the records and the write path |
| people-privilege | The caller, the admin gate and the read projections |
| events-api | Services, websocket commands, events and the API surface model |
| open-library-covers | Open Library, the lookup queue and the cover files |
| open-library-contribute | The Open Library account and the send of a book |
| scan-and-isbn | ISBN rules, the camera scanner and the scan service |
| csv-import-export | Goodreads, StoryGraph and library CSV files |
| loans-home-keeper | Loans and their Home Keeper tasks |
| wishlist-todo-sync | The wishlist and its per-person to-do list |
| entities | The coordinator, the sensors and the To read lists |
| home-keeper-dependency | The check, the tab, the companion and the repairs |
| frontend-tab-card | The tab, the card, routing, i18n and the bundles |
Trade-offs
- A tab in the Home Keeper panel over a sidebar panel of its own: 1 admin place for the home, and Home Keeper owns the history and the layout. The cost is the dependency.
- Services registered at entry setup over services registered in
async_setup: the teardown reads the model, so registration and removal cannot disagree. A call during a reload gets "action not found". - 1 document for the whole library over a file for each section: 1 save is 1 consistent state. A library of 5000 books is the design size.
One-way doors
- The domain
home_keeper_library, the static path/home_keeper_library_staticand the tab path/home-keeper/library. - The service names and fields, and the event names and payloads (INTEGRATING.md).
- The storage key
home_keeper_library,const.STORAGE_VERSIONand the option keycurrency.