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
- Resilience
- Actions and events
- 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.
- Survives an outage across a restart. The last inventory that parsed is kept on disk and shown if Home Assistant restarts while CellarTracker is unreachable, instead of every sensor reading unavailable. See Resilience.
- Backs off politely. HTTP 429 and 5xx slow the poll down exponentially, with jitter, and the configured schedule returns by itself once CellarTracker answers.
- Two actions —
cellar_tracker.refreshandcellar_tracker.get_wine_by_bin— and an event,cellartracker_inventory_changed. See Actions and events. - Diagnostics you can attach to a bug report: credentials, account name and rack names are kept out of it.
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.
The inventory is kept in Home Assistant's .storage/ directory
(cellar_tracker.inventory_cache_<entry id>): rewritten whenever it changes, and — so that its
timestamp stays true for a cellar that does not — at least once an hour for an unchanged one. If
Home Assistant restarts while CellarTracker is unreachable, the integration starts from that
copy instead of leaving every sensor unavailable until the outage ends.
- Only ever a stand-in for the first refresh. Once there is data, a failing poll behaves as it always did: the entities go unavailable rather than quietly showing something older than what they already had.
- Never used for a login failure. Bad credentials need you, and old data would hide the re-authenticate prompt.
- It says it is stale. Last synchronised shows when the cache was written, not now, so you can see how old the numbers are. While serving it the next poll comes sooner — the smaller of your interval and 15 minutes — and a live poll restores your schedule.
- It also guards the first poll after a restart. The cache counts as history, so an empty or drastically smaller first response is treated as suspect against the cached stock, exactly as it would be mid-run: it is refused once (and the cache is kept, not overwritten) and believed if it repeats. Without that, a transient blip right after a restart would have been recorded as an empty cellar.
- Only a response that parsed is ever cached. An error page cannot become the "last known" cellar.
- It is private and it is removed with the integration. It holds your purchase history and notes, so the file is created private, contains no credentials, and is deleted when you remove the integration.
When CellarTracker answers 429 or 5xx the integration waits longer before asking again:
the delay doubles with each consecutive failure up to six hours, with a little random jitter so
that installs throttled together do not all return together. It is never sooner than the
interval you configured, and a Retry-After from the server is honoured exactly. Once
CellarTracker answers, your schedule comes back on its own. Other errors — a timeout, a refused
connection, a 404 — keep your schedule: they are about the connection, not about load.
Settings → Devices & Services → CellarTracker → ⋮ → Download diagnostics. The report includes whether the cache is being served, the backoff state, why the last poll failed, the drink-window counts, and the export's column names — which is how a change to CellarTracker's format shows up without anyone sending a copy of their cellar. It leaves out your password, your username, your rack names, and everything about a bottle beyond a short allowlist.
Neither creates an entity. They give automations the cellar without putting a state in the recorder for every bottle.
Fetches the inventory now, instead of at the next scheduled poll. It goes through Home Assistant's own refresh debouncer, so calling it in a loop cannot hammer CellarTracker.
action: cellar_tracker.refreshReturns the bottles in a rack bin, for a script or automation to act on. It returns a
response, so call it with response_variable.
- action: cellar_tracker.get_wine_by_bin
data:
bin: "A1"
location: "Cellar" # optional; omit to search every location
response_variable: rack
- action: notify.persistent_notification
data:
message: >
{% for wine in rack.bottles %}
{{ wine.name }} {{ wine.vintage or 'NV' }} — {{ wine.drink_status }}
{% else %}
Bin A1 is empty.
{% endfor %}Each bottle in rack.bottles has name, vintage, wine_id, location, bin,
drink_window (begin and end), drink_status (ready, past, aging or unknown),
peak and unique_bottle_id. A non-vintage wine has vintage: null rather than 0, and a bottle
with no recorded window has null years. Bins are matched ignoring case and surrounding spaces,
an empty bin matches nothing, and an empty location means the same as leaving it out — an
automation that renders an empty template searches every location rather than only unplaced
bottles. The tasting note, the price paid and the barcode are
deliberately left out: responses land in automation traces, which are shared far more freely than
the cellar itself.
peak is true in the middle third of a bottle's drinking window: with
span = EndConsume - BeginConsume, from BeginConsume + span/3 to EndConsume - span/3. The
first and last thirds are still ready to drink — just not at their best — so the opening and
closing years of a window are not peak. The one exception is a one-year window
(BeginConsume equal to EndConsume): its only year is both, and it is peak in that year. For a window of 2020–2030 that is 2024 to 2026. It is never true for
a window with only one end, since there is no span to divide, and it is always a subset of
ready. A bottle is peak only while its drink_status is ready.
Fired when the set of bottles changes between two successful polls.
triggers:
- trigger: event
event_type: cellartracker_inventory_changed
actions:
- action: persistent_notification.create
data:
message: >
{{ trigger.event.data.added_count }} added,
{{ trigger.event.data.removed_count }} removed —
{{ trigger.event.data.total_count }} in the cellar.The payload has added_bottles, removed_bottles and total_count, plus added_count,
removed_count and truncated. Each list holds at most 50 bottles, each with only
unique_bottle_id, iWine, Wine, Vintage, Location and Bin — a large first sync cannot
push hundreds of kilobytes through the event bus, and truncated tells you if the lists were cut.
It deliberately does not fire when there is no history to compare with — a fresh install, or after the cache has been deleted — nor for a revaluation or an edited note, nor for a failed poll. Moving a bottle to another bin is reported as removed from the old one and added to the new.
A restart does remember. After one, the first live poll is compared against the cached inventory, so bottles added, removed or moved while Home Assistant was offline are announced. If the inventory is unchanged, nothing fires.
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.
For a script, cellar_tracker.get_wine_by_bin returns the bottles in a rack bin, drinking
window included — see Actions and events.
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. After a restart the sensors show the cached inventory instead, with Last synchronised showing how old it is.
- "CellarTracker asked us to slow down" — HTTP 429 or 5xx. The poll backs off and your schedule returns by itself.
- "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.
- "returned N bottles but the cellar previously held M; treating it as a truncated export" — the response held fewer than half the bottles it did a moment ago, which is how a cut-off download looks. It is refused once so a half-finished export cannot replace your inventory, overwrite the cache or announce the missing bottles as removed. If you really did sell or drink that much, the next refresh accepts it.
No — 900 seconds is enforced. Each refresh downloads your entire inventory, and CellarTracker is
a small service. If you need a value right now, call cellar_tracker.refresh (see
Actions and events) or 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 .
ruff format --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 mypyIt runs under mypy --strict. Running 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.