Brings your CellarTracker wine cellar into Home Assistant: summary sensors for bottle count, cellar value and drinking windows, plus a dashboard that filters every bottle down to the ones you can drink tonight.
Disclaimer This is a personal project. It is not affiliated with, connected to, or endorsed by CellarTracker! LLC. "CellarTracker!" is a trademark of CellarTracker! LLC.
- Overview
- What gets created
- Bottle-level data
- Installation via HACS
- Manual installation
- Configuration
- The dashboard
- Lovelace and automation examples
- Troubleshooting and FAQ
- Development
- License
The integration is a standard modern custom component: UI config flow, a
DataUpdateCoordinator for polling, and entities grouped under a device per account.
How it fetches data. One request per refresh to CellarTracker's xlquery.asp export
endpoint for the Inventory table in tab-separated format, returning every bottle you own
with 66 columns each. The request uses Home Assistant's shared aiohttp session under a
60-second asyncio.timeout, so a hung server is cancelled cleanly rather than parking a
worker thread. Parsing runs in an executor, so the event loop is never blocked. The
cellartracker library supplies the endpoint URL
and error semantics; its own requests-based transport sets no timeout and is not used.
Features
- Two summary sensors — total bottle count and total cellar value — with proper device classes, units and state classes, so both feed Home Assistant's long-term statistics.
- Two drink-window sensors — how many bottles are ready to drink, and how many are past their window — derived at poll time, so they cost nothing and add no entity per bottle.
- A selectable currency, so the value sensor is denominated correctly.
- A diagnostic sensor reporting when the cellar last synchronised.
- A REST endpoint exposing full per-bottle detail, and a self-contained dashboard page that
filters it by drinking window, searches it across five fields and shows each bottle's position
in its window. The page ships with the integration and is served from it, so there is nothing
to copy into
<config>/www. - Reauthentication: if your password changes, Home Assistant prompts you to re-enter it rather than silently failing.
- One account per installation, enforced by the config flow, so there is no ambiguity about which cellar an entity or endpoint refers to.
- Upstream error pages are rejected rather than being recorded as a genuine zero, so an outage cannot punch a hole in your cellar-value history.
Adding the integration creates one device per account with five entities. It does not create an entity per bottle — see Bottle-level data for why, and for how to reach that data.
| Entity | Example | Unit | Device class | State class |
|---|---|---|---|---|
| Total bottles | 142 |
bottles |
— | measurement |
| Total value | 9812.50 |
your chosen currency | monetary |
total |
| Ready to drink | 37 |
bottles |
— | measurement |
| Past drinking window | 4 |
bottles |
— | measurement |
| Last synchronised | 2026-08-28 09:30:00 |
— | timestamp |
diagnostic |
Upgrading from 0.0.17 or earlier: the diagnostic entity that reported Connected now reports
when the cellar last synchronised. It keeps its entity ID, so nothing has to be repointed. Its old
value never changed once the integration was running, so nothing could have been triggering on it.
The device is named after the account, so entity IDs follow the account name:
sensor.<account>_total_bottles
sensor.<account>_total_value
sensor.<account>_ready_to_drink
sensor.<account>_past_drinking_window
sensor.<account>_last_synchronised
That third ID is what a fresh install gets. An install that predates 0.0.18 keeps
sensor.<account>_status, because Home Assistant assigns an entity ID once, at first
registration, and never rewrites it. Both point at the same entity; only the name differs.
If you installed before v0.0.15, your entity IDs were generated when the entities were first
registered and Home Assistant keeps them — they will still be sensor.cellartracker_total_bottles
and friends. Existing dashboards and automations keep working; only the display names change.
Check Settings → Devices & Services → CellarTracker → entities for the exact IDs on your
system, and use those in the examples below.
Per-bottle detail is exposed through an authenticated REST endpoint rather than as entities or state attributes:
GET /api/cellartracker/inventory # every bottle, as JSON
GET /api/cellartracker/inventory?view=compact # the same bottles, ten columns
GET /api/cellartracker/settings # the configured currency and its symbol
Why not one entity per bottle? Home Assistant's recorder writes a row for every state and attribute change of every entity. A 500-bottle cellar would mean 500 entities whose valuations drift constantly, which bloats the database for data that is reference material rather than something you automate on. Putting the full list in a state attribute has the same problem, only worse — attributes are recorded with every state write.
Each bottle in the response carries CellarTracker's own column names. The ones the dashboard uses:
| Field | Meaning |
|---|---|
iWine |
CellarTracker's wine ID — links to the wine's page |
Wine |
Wine name |
Vintage |
Vintage year, 0 for non-vintage |
Producer |
Producer name |
Location / Bin |
Where the bottle is stored |
Barcode |
The bottle's barcode, if you have scanned one |
Size |
Bottle size, e.g. 750ml |
Valuation |
Current value, coerced to a float (0.0 if unparseable) |
Price |
What you paid |
PurchaseDate |
Purchase date |
BeginConsume / EndConsume |
Drink window — the first and last recommended year |
Country / Region / SubRegion / Appellation |
Origin |
Type / Color / Varietal / MasterVarietal |
Style |
BottleNote, CNotes, PNotes |
Notes |
WA, WS, IWC, JR, … |
Critic scores |
unique_bottle_id |
Added by this integration: a stable per-bottle identifier |
The response contains all 66 columns CellarTracker returns; the table above is the useful subset.
The drink-window columns are named
BeginConsumeandEndConsume, notbegin_drink/end_drink. They hold years as strings, e.g."2018", and are empty when CellarTracker has no recommendation.
Requires Home Assistant 2024.11 or newer — 2024.7 added the static-path API the integration
uses to serve its dashboard page, and 2024.11 added the config_entry argument its data
coordinator now passes.
- Open HACS in Home Assistant.
- Click the ⋮ menu (top right) → Custom repositories.
- Add:
- Repository:
https://github.com/GuvHas/cellartracker - Type:
Integration
- Repository:
- Click Add, then close the dialog.
- Search HACS for CellarTracker and click Download.
- Restart Home Assistant.
That is the whole installation. There is no file to copy: the dashboard page ships inside the
integration and Home Assistant serves it at /cellartracker/cellar.html as soon as the
integration loads.
Then continue to Configuration.
-
Download or clone this repository.
-
Copy the integration folder into your Home Assistant config directory:
custom_components/cellar_tracker/ → <config>/custom_components/cellar_tracker/Note the directory is
cellar_tracker, with an underscore — it must match the integration's domain exactly. -
Your config directory should now contain:
<config>/ └── custom_components/ └── cellar_tracker/ ├── __init__.py ├── cellar_data.py ├── config_flow.py ├── const.py ├── manifest.json ├── sensor.py ├── strings.json ├── views.py ├── translations/ │ └── en.json └── www/ └── cellar.htmlCopy the folder whole —
www/cellar.htmlis the dashboard page, and the integration serves it from there. Nothing goes into<config>/www. -
Restart Home Assistant.
All configuration is through the UI. There is nothing to put in configuration.yaml.
-
Go to Settings → Devices & Services.
-
Click + Add Integration (bottom right).
-
Search for CellarTracker and select it.
-
Fill in the form:
Field Notes Username Your CellarTracker username Password Your CellarTracker password Seconds between refreshes Default 21600(6 hours). Minimum900(15 minutes).Currency The currency your cellar value is reported in -
Click Submit. Credentials are verified against CellarTracker before the entry is created, so a mistake is reported immediately rather than after the first failed poll.
Settings → Devices & Services → CellarTracker → Configure lets you change the refresh interval and currency. The integration reloads automatically.
Changing your password is handled by re-authentication rather than by an editable form: once CellarTracker starts rejecting the stored password, the integration flags it and Home Assistant surfaces a Re-authenticate prompt on the CellarTracker card in Devices & Services. Enter the new password there and only the password is replaced — the refresh interval, currency, entity IDs and history all stay as they were.
There is no proactive "change my password now" form. If you would rather not wait for the next refresh to notice, use ⋮ → Reload on the integration to trigger one immediately.
The config flow allows a single CellarTracker account. Adding it a second time aborts rather than creating a duplicate. To switch accounts, delete the existing entry and add it again.
The default is deliberately conservative. A cellar changes slowly, the export endpoint returns your entire inventory in one request, and CellarTracker is a small service run for enthusiasts — polling it every minute is neither useful nor neighbourly. Fifteen minutes is the enforced floor.
The page is served by the integration itself, from wherever the integration was installed. Add an iframe card pointing at it:
type: iframe
url: /cellartracker/cellar.html
aspect_ratio: 100%
title: My Wine CollectionThe page reads your live Home Assistant session from the parent frame, so it needs no token or credential of its own. Open it embedded in a dashboard, not as a standalone browser tab.
Four filter chips, each carrying its count, so a 150-bottle cellar becomes a short list:
| Chip | Shows |
|---|---|
| All wines | Everything, including bottles with no recorded window |
| Ready to drink | The current year falls inside the drinking window |
| Past window | The window ended before this year |
| Needs aging | The window has not opened yet |
The Ready and Past counts are the same numbers the sensors report — they are checked against each other by the test suite, so the chip and the sensor card beside it cannot disagree.
Search across wine name, vintage, location, bin and barcode. Every word has to match, so
chianti 2023 narrows rather than finding nothing.
Sort by wine, vintage, value, drink-by year, bin or location, in either direction. Bottles with nothing recorded for the chosen column sort last rather than heading the list.
Each bottle shows its name, vintage, location and bin, its value in your configured currency, and a bar showing where this year sits inside its drinking window:
| Label | Meaning |
|---|---|
| Ready (green) | Inside the drinking window |
| Drink this year (amber) | The final year of the window — urgent, not expired |
| Past window (red) | The window ended before this year |
| Needs aging (blue) | Not open yet |
| No window (grey) | CellarTracker has no recommendation for this bottle |
Tapping a bottle opens a drawer with its barcode, full window, value, a Copy bin button and a link to the wine on CellarTracker.
The page follows your Home Assistant light/dark theme, and every control is sized for a thumb.
The card needs no account parameter — one account is supported per installation, so the
endpoints have nothing to disambiguate. A stale ?entry_id=... left over from a card configured
against v0.0.16 is accepted and ignored, so those cards keep working unchanged.
Upgrading from before v0.0.16? You once had to copy the page into <config>/www yourself.
That copy still works — /local/cellar.html is Home Assistant's own static mount and this change
does not touch it — so existing cards keep rendering. It is a stale copy, though: it will not pick
up fixes to the page. Point your card at /cellartracker/cellar.html and delete
<config>/www/cellar.html when convenient.
Replace <account> with your device name — or with cellartracker if you installed before
v0.0.15. Check the exact entity IDs under Settings → Devices & Services → CellarTracker.
type: entities
title: Wine Cellar
entities:
- entity: sensor.<account>_total_bottles
name: Bottles
- entity: sensor.<account>_total_value
name: Cellar value
- entity: sensor.<account>_ready_to_drink
name: Ready to drink
- entity: sensor.<account>_last_synchronised
name: Last synchronisedtype: markdown
content: >
## 🍷 The Cellar
**{{ states('sensor.<account>_total_bottles') }}** bottles worth
**{{ states('sensor.<account>_total_value') }}
{{ state_attr('sensor.<account>_total_value', 'unit_of_measurement') }}**
{% set bottles = states('sensor.<account>_total_bottles') | int(0) %}
{% set value = states('sensor.<account>_total_value') | float(0) %}
{% if bottles > 0 %}
Average bottle value: **{{ (value / bottles) | round(2) }}**
{% else %}
The cellar is empty.
{% endif %}The value sensor is device_class: monetary with state_class: total, so Home Assistant records
long-term statistics for it:
type: statistics-graph
title: Cellar value
entities:
- sensor.<account>_total_value
stat_types:
- mean
days_to_show: 365
period: dayautomation:
- alias: "Cellar inventory changed"
triggers:
- trigger: state
entity_id: sensor.<account>_total_bottles
conditions:
# Ignore startup and unavailability, and only fire on a real change.
- condition: template
value_template: >
{{ trigger.from_state.state not in ['unknown', 'unavailable', none]
and trigger.to_state.state not in ['unknown', 'unavailable', none]
and trigger.from_state.state != trigger.to_state.state }}
actions:
- action: notify.persistent_notification
data:
title: "Wine cellar updated"
message: >
{% set before = trigger.from_state.state | int(0) %}
{% set after = trigger.to_state.state | int(0) %}
{% if after > before %}
{{ after - before }} bottle(s) added — {{ after }} in the cellar.
{% else %}
{{ before - after }} bottle(s) consumed — {{ after }} remaining.
{% endif %}
mode: singleautomation:
- alias: "Cellar value moved more than 10%"
triggers:
- trigger: state
entity_id: sensor.<account>_total_value
conditions:
- condition: template
value_template: >
{% set before = trigger.from_state.state | float(0) %}
{% set after = trigger.to_state.state | float(0) %}
{{ before > 0 and (after - before) | abs / before > 0.1 }}
actions:
- action: notify.persistent_notification
data:
title: "Cellar revaluation"
message: >
Value moved from {{ trigger.from_state.state }} to
{{ trigger.to_state.state }}.
mode: singleautomation:
- alias: "CellarTracker is not responding"
triggers:
- trigger: state
entity_id: sensor.<account>_total_bottles
to: "unavailable"
for: "12:00:00"
actions:
- action: notify.persistent_notification
data:
title: "CellarTracker unavailable"
message: "No successful refresh for 12 hours. Check the logs."
mode: singleA common request is an auto-entities or Markdown card listing wines currently in their drink
window. That is not possible with the entities this integration creates, because there are no
per-bottle entities to filter — auto-entities works over the entity registry, and bottles are
not in it.
There are two answers that do not need per-bottle entities.
For a count, use the sensors: sensor.<account>_ready_to_drink and
sensor.<account>_past_drinking_window are ordinary numeric entities, so they work in any card,
template or automation.
For the actual list, use the dashboard page and its Ready to drink chip. That is what the chips are for, and the counts match the sensors exactly.
If you want the bottles themselves in Lovelace proper, the missing piece is per-bottle entities — see Bottle-level data for why they are not created by default. Please open an issue if this matters to you; it is a reasonable feature to add behind an opt-in, given the recorder cost is the user's to accept.
Credentials are checked against CellarTracker before the entry is created. Confirm you can log in at cellartracker.com with the same details. The username is your CellarTracker username, not the email address you sign in with, if those differ.
CellarTracker rejected the stored credentials — usually a password change. Enter the new password in the prompt. Nothing else needs updating; only the password is replaced.
A refresh failed. The integration keeps the last good values and marks the entities unavailable
rather than publishing a wrong number. Check Settings → System → Logs for cellar_tracker:
- "Cannot reach CellarTracker" — network or an outage upstream. It retries on the next cycle.
- "unrecognised row(s) with no 'iWine' column" — CellarTracker returned something that was not inventory data, typically a maintenance or error page. It recovers on its own.
- "returned no inventory rows but the cellar previously held N bottles" — a zero reading right after a stocked cellar is treated as an error the first time. If you genuinely emptied your cellar, the next refresh accepts it and the sensors go to zero.
No — 900 seconds is enforced. Each refresh downloads your entire inventory, and CellarTracker is a small service. If you need a value right now, use ⋮ → Reload on the integration.
Check the URL: it is /cellartracker/cellar.html, served by the integration. If Home Assistant
returns 404 there, the integration has not finished loading — look under Settings → Devices &
Services — or the page is missing from the install, which the log reports as
Dashboard page ... is missing. Re-download the integration in HACS, or re-copy the
cellar_tracker folder whole if you installed manually.
/local/cellar.html is the pre-v0.0.16 location and only works if you copied the page into
<config>/www yourself. It is not created for you.
The page could not read your Home Assistant session. Almost always this means it was opened as a standalone browser tab rather than embedded in an iframe card. Use the card described in The dashboard. The page is deliberately unauthenticated static content; the data behind it is not, so the API calls it makes need your session.
No. One account per installation is enforced: a second attempt aborts with "CellarTracker is already configured". Remove the existing entry under Settings → Devices & Services first if you want to switch accounts.
Deprecated. It still works, but a Home Assistant long-lived token grants full account access and never expires, so a URL carrying one leaks it into browser history, server logs and screenshots. The page now moves any token it finds into session storage and strips it from the address bar. Embedded as an iframe card, no token is needed at all.
The currency is a display setting — it labels the number CellarTracker reports, it does not
convert it. Set it to match the currency your CellarTracker account values bottles in, under
Configure. Bottles whose Valuation cannot be parsed count as 0, so a wine CellarTracker
has no valuation for contributes nothing rather than breaking the total.
Deliberately not there — see Bottle-level data. Use
/api/cellartracker/inventory.
logger:
default: warning
logs:
custom_components.cellar_tracker: debug
cellartracker: debugpip install -r requirements_test.txt
python -m pytest
ruff check .The test suite stubs the handful of homeassistant symbols the integration imports rather than
depending on pytest-homeassistant-custom-component, so it installs in seconds and runs in a few.
The dashboard tests execute cellar.html's real script under Node, so install Node to run them —
they skip if it is absent.
Type checking needs Home Assistant itself, which the test suite deliberately does not install:
python3.13 -m venv .typecheck
.typecheck/bin/pip install -r requirements_mypy.txt
.typecheck/bin/python -m mypyRunning mypy against an interpreter without Home Assistant is worse than not running it: every
homeassistant.* import resolves to Any, and the check passes over code it never looked at.
The configuration refuses to do that, so it will fail loudly rather than mislead you.
CI runs the suite on Python 3.12 and 3.13, plus ruff, mypy, hassfest and HACS validation.
Releases are published by .github/workflows/release.yml, which refuses to tag a commit whose
manifest.json version disagrees with the release, or whose tests and lint do not pass.
- Bump
versionincustom_components/cellar_tracker/manifest.json. - Optionally write
release_notes/<version>.md. If it exists the workflow uses it verbatim; otherwise GitHub generates notes from the commit history. Write them by hand whenever a version changes how the integration is installed or configured — 0.0.16 moved the dashboard URL, which no generated changelog would have made obvious. - Merge to
main, then either push the tag (git tag 0.0.17 && git push origin 0.0.17) or run Actions → Release → Run workflow and enter the version. The second path creates the tag for you, which is what to use when your client cannot push tag refs.
Tags are bare version numbers with no v prefix, optionally with a single-letter suffix
(0.0.13b), matching every release since 0.0.10. Re-running a dispatch for a version whose tag
already exists is safe: the workflow checks that tag out and validates it, rather than validating
the branch and publishing the tag.
The cellartracker library calls requests.get(url, params) with no timeout=
(api.py), so the
socket has no deadline. Running that on an executor thread means an application-level timeout can
stop Home Assistant waiting, but cannot interrupt the worker: concurrent.futures has no way to
cancel a thread that is already running, so it stays in recv() until the OS gives up. For a
server that accepts a connection and then never replies, that is the TCP keepalive interval —
7200 seconds by default — with the account password sitting in the thread's stack frame.
So the integration does its own HTTP with Home Assistant's shared aiohttp session, where
cancellation genuinely cancels and no thread is involved. The library still supplies the endpoint
URL, the not-logged-in marker, the table and format enums, and the exception types: it owns the
contract, just not the transport.
Possible future contribution. Adding timeout= to cellartracker's api.py would fix this
at the root for every consumer — roughly:
DEFAULT_TIMEOUT = 60
def execute(self, url=BASE_URL, params={}, timeout=DEFAULT_TIMEOUT):
...
reponse = requests.get(url, params, timeout=timeout)That is worth submitting upstream if anyone feels like it, but this integration does not depend on it — it no longer calls that code path at all. Noted here so the reasoning is not lost.
MIT. "CellarTracker!" is a trademark of CellarTracker! LLC; this project is not affiliated with them.