My Smart Home

How to Build a Local Alcohol Assistant Integration for Home Assistant

How to build a Home Assistant custom integration that reads local Grocy bar stock, plans cocktails with substitutions, records pours, and powers a Lovelace drink menu.

On this page, 20 sections
  1. How the pieces fit together
  2. What you need
  3. Start with the integration skeleton
  4. Create a UI config flow
  5. Build a narrow asynchronous Grocy client
  6. Model ingredients as a graph, not a flat category
  7. Keep substitutions outside recipes
  8. Use canonical, machine-readable recipes
  9. Plan the whole drink before promising it
  10. Coordinate polling through one data coordinator
  11. Treat consumption as a transaction, even when Grocy cannot
  12. Expose actions, not giant writable sensors
  13. Serve large menus through authenticated views
  14. Add a local dashboard card
  15. Set up the entry cleanly
  16. Test the dangerous parts first
  17. Install it manually first
  18. Package it for HACS
  19. Common mistakes
  20. Where to take it next

An Alcohol Assistant has one job that sounds simple and isn’t: what can I make with the bottles and mixers I have right now?

Answering it well takes more than matching recipe words against product names. The integration has to know that bourbon is a whiskey, that one rye recipe will happily take bourbon while another shouldn’t, convert Grocy stock units into recipe units, reserve enough of every ingredient, and then deduct the exact bottles you picked. This is the engine behind the Barkeep voice bartender on my bar tablet, and here’s how you’d build your own.

The guide builds it as a local Home Assistant custom integration. Grocy stays the inventory source of truth. Home Assistant handles setup, polling, actions, sensors, and the dashboard. A small domain engine handles taxonomy, substitutions, recipe planning, and stock allocation.

It’s based on a system I run, but the examples are deliberately generic - no household addresses, API keys, product IDs, or private recipe catalogs in here. Mine is a vibe-coded build you can’t download, so treat this as a blueprint rather than an install guide.

Keep Grocy as the inventory source of truth, put the drink logic in a pure Python engine with no Home Assistant imports, plan every ingredient of a recipe against one temporary stock ledger before promising it, and re-fetch live stock before you deduct anything.

Responsible-use note: This project organizes an adult household’s bar inventory and recipes. It should not encourage excessive drinking, make health claims, or present alcohol to minors. Add prominent nonalcoholic choices and never treat inventory software as a substitute for personal judgment.

How the pieces fit together

Grocy REST API
      |
      v
async Grocy client  ---> normalized stock in milliliters
      |
      v
DataUpdateCoordinator
      |
      +-- taxonomy graph
      +-- substitution graph
      +-- recipe catalog
      +-- allocation engine
      |
      +--> small Home Assistant sensors
      +--> authenticated JSON views for large menus
      +--> Home Assistant actions for pours and recipes
      +--> local dashboard cards

If you get one thing right in this whole build, make it the boundary between the Home Assistant shell and the drink engine.

The shell knows about config entries, the event loop, coordinators, entities, actions, HTTP views, and frontend resources. The engine knows about ingredients, bottles, recipes, substitutions, quantities, and allocations. Keep the engine in ordinary Python with no Home Assistant imports. You can then test thousands of recipe combinations without starting Home Assistant.

What you need

  • A working Home Assistant installation with access to its config directory.
  • A local Grocy server and a Grocy API key.
  • Bar products entered in Grocy with reasonably consistent names and units.
  • Basic familiarity with Python, JSON, and Home Assistant logs.
  • A development Home Assistant instance or a complete backup before testing stock deductions.

Grocy exposes its API documentation in its web interface and publishes an OpenAPI specification. Use that specification for the Grocy version you run; do not assume an old endpoint example is permanent.

Start with the integration skeleton

Home Assistant loads a custom integration from <config>/custom_components/<domain>. The official file-structure guide is the best reference for the required files.

Use a focused structure instead of putting the entire project in __init__.py:

custom_components/
└── alcohol_assistant/
    ├── __init__.py
    ├── manifest.json
    ├── const.py
    ├── config_flow.py
    ├── coordinator.py
    ├── api.py
    ├── models.py
    ├── catalog.py
    ├── engine.py
    ├── sensor.py
    ├── button.py
    ├── todo.py
    ├── actions.py
    ├── http.py
    ├── services.yaml
    ├── strings.json
    ├── translations/
    │   └── en.json
    ├── frontend/
    │   └── alcohol-assistant-card.js
    └── data/
        ├── ingredients.json
        ├── substitutions.json
        └── recipes.json

The domain must be lowercase and should match the directory name everywhere. Here is a practical manifest.json starting point:

{
  "domain": "alcohol_assistant",
  "name": "Alcohol Assistant",
  "codeowners": ["@your-github-name"],
  "config_flow": true,
  "dependencies": ["http"],
  "documentation": "https://example.com/alcohol-assistant",
  "integration_type": "service",
  "iot_class": "local_polling",
  "issue_tracker": "https://github.com/your-name/alcohol-assistant/issues",
  "requirements": [],
  "single_config_entry": true,
  "version": "0.1.0"
}

Custom integrations need a version. single_config_entry is appropriate when one Home Assistant installation will talk to one household bar. Remove it if you deliberately support several Grocy servers. See the manifest documentation before publishing because manifest fields and validation rules can change.

Create a UI config flow

Do not ask users to edit YAML for a server URL and API key. A config flow gives them a normal Settings → Devices & services → Add integration experience.

Ask for:

  • Grocy URL, such as http://grocy.local.
  • Grocy API key.
  • An optional scan interval in the options flow.
  • Optional behavior such as treating carbonated water as unlimited.

Normalize the URL once, validate it by requesting Grocy’s system information, and only then create the entry. Store connection credentials in entry.data; store changeable preferences in entry.options.

class AlcoholAssistantConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
    VERSION = 1

    async def async_step_user(self, user_input=None):
        errors = {}
        if user_input is not None:
            url = normalize_grocy_url(user_input[CONF_URL])
            client = GrocyClient(
                async_get_clientsession(self.hass),
                url,
                user_input[CONF_API_KEY],
            )
            try:
                await client.async_validate()
            except InvalidAuth:
                errors["base"] = "invalid_auth"
            except CannotConnect:
                errors["base"] = "cannot_connect"
            else:
                await self.async_set_unique_id(url)
                self._abort_if_unique_id_configured()
                return self.async_create_entry(
                    title="Alcohol Assistant",
                    data={**user_input, CONF_URL: url},
                )

        return self.async_show_form(
            step_id="user",
            data_schema=CONFIG_SCHEMA,
            errors=errors,
        )

Add reauthentication before release so an expired or replaced API key can be repaired without deleting the integration. Home Assistant’s config-flow documentation covers current flow, options, and reauthentication patterns.

Build a narrow asynchronous Grocy client

Use Home Assistant’s shared aiohttp session. The API client should return normalized domain objects rather than passing raw Grocy responses into the rest of the integration.

Its responsibilities are intentionally limited:

  1. Validate the server and API key.
  2. Fetch products, stock, quantity units, and locations.
  3. Classify only products that can safely participate in the bar.
  4. Convert product stock into milliliters.
  5. Consume a known quantity of a known Grocy product.
class GrocyClient:
    def __init__(self, session, base_url: str, api_key: str) -> None:
        self._session = session
        self._api_url = f"{base_url.rstrip('/')}/api"
        self._headers = {"GROCY-API-KEY": api_key}

    async def _get(self, path: str):
        async with self._session.get(
            f"{self._api_url}/{path}", headers=self._headers
        ) as response:
            response.raise_for_status()
            return await response.json()

    async def async_validate(self) -> None:
        await self._get("system/info")

    async def async_consume(self, product_id: int, amount: float) -> None:
        # Build this payload from the OpenAPI definition for your Grocy version.
        await self._post(f"stock/products/{product_id}/consume", {"amount": amount})

Never infer bottle capacity from wishful thinking. Give the client explicit metadata it can trust. One simple convention is to put machine-readable lines in a Grocy product description:

alcohol-assistant: bourbon
alcohol-assistant-volume-ml: 750

The first line maps the product to a canonical ingredient. The second says that one Grocy stock unit represents a 750 ml bottle. A syrup made in batches might instead use alcohol-assistant-yield-ml.

Support carefully chosen package-name fallbacks such as 750 ml, 1 L, and 12 fl oz, but prefer explicit metadata. Knowing you’ve got three bottles tells you nothing about how much liquid is actually in them.

For a few ordinary grocery ingredients - lemons, cream, eggs, honey - you can allow conservative name-based classification. Require both positive patterns and exclusion patterns. For example, cream should not silently match cream soda, and orange should not automatically match an orange-flavored liqueur.

Model ingredients as a graph, not a flat category

A spirit can have more than one useful parent. Bourbon is both a bourbon and an American whiskey; an aged rum may be both aged rum and rum. A tree forces you to choose one ancestry. A directed acyclic graph lets an ingredient have multiple parents.

{
  "id": "bourbon",
  "name": "Bourbon",
  "parents": ["american-whiskey", "whiskey"],
  "kind": "spirit",
  "patterns": ["\\bbourbon\\b"]
}

When loading the catalog:

  • Reject missing parent IDs.
  • Detect cycles.
  • Precompute every ingredient’s ancestor closure.
  • Prefer the most specific matching descendant when a product could match several nodes.
  • Keep regular expressions anchored and explainable.

Precomputing ancestry once makes planning fast. The engine can ask whether bottled-in-bond-bourbon satisfies a whiskey requirement without repeatedly walking the graph.

Keep substitutions outside recipes

Let recipes just say what they call for, and keep all the substitution rules in a separate graph.

{
  "from": "rye-whiskey",
  "to": "bourbon",
  "rank": 20,
  "fidelity": 0.82,
  "multiplier": 1.0,
  "layer": "style"
}

Each edge says that the ingredient in to may satisfy a missing from requirement. rank controls preference, fidelity records how close the result is to the intended drink, multiplier adjusts quantity, and layer lets you separate near-equivalents from broader fallbacks.

Use ordered layers such as:

  1. Exact product or descendant match.
  2. Near-equivalent style substitution.
  3. Same-family substitution.
  4. Broad fallback, if the recipe allows one.

Reject recipes that embed substitution instructions in ingredient fields. Otherwise your policy becomes duplicated and contradictory.

Use canonical, machine-readable recipes

Keep human prose for instructions, but make the ingredient list deterministic:

{
  "id": "simple-whiskey-sour",
  "name": "Simple Whiskey Sour",
  "servings": 1,
  "ingredients": [
    {"ingredient": "whiskey", "amount_oz": 2.0},
    {"ingredient": "lemon-juice", "amount_oz": 0.75},
    {"ingredient": "simple-syrup", "amount_oz": 0.75}
  ],
  "instructions": [
    "Shake with ice.",
    "Strain over fresh ice."
  ]
}

Convert fluid ounces to milliliters at the catalog boundary and use milliliters internally. That gives the engine one unit for comparisons and allocations while recipe authors retain familiar bar measurements.

Allow an optional custom catalog in the Home Assistant configuration directory, for example:

<config>/alcohol_assistant_recipes.json

Version and validate its format. Reject duplicate recipe IDs and invalid ingredient references before replacing the active catalog.

Plan the whole drink before promising it

For every recipe requirement, the engine should:

  1. Find exact products and taxonomy descendants.
  2. If necessary, follow allowed substitution edges in rank order.
  3. Filter out candidates without enough stock.
  4. Reserve stock in a temporary allocation ledger.
  5. Continue only if every requirement can be allocated.
  6. Return the selected products, quantities, substitutions, and fidelity score.

Do not independently answer “is each ingredient available?” Two requirements may compete for the same final 30 ml. A recipe is available only if one complete allocation exists.

Model the plan explicitly:

@dataclass(frozen=True)
class Allocation:
    requirement_id: str
    product_id: int
    ingredient_id: str
    amount_ml: Decimal
    substitution_path: tuple[str, ...]
    fidelity: float

@dataclass(frozen=True)
class RecipePlan:
    recipe_id: str
    available: bool
    allocations: tuple[Allocation, ...]
    missing: tuple[str, ...]
    fidelity: float

The planner should be a pure function. Give it a catalog and a stock snapshot; receive plans back. And because it’s pure, it’s the easiest part of the whole thing to test properly - no Home Assistant required.

Coordinate polling through one data coordinator

Do not let every sensor poll Grocy independently. Use one DataUpdateCoordinator to fetch stock and compute the shared presentation model.

class AlcoholCoordinator(DataUpdateCoordinator[AlcoholData]):
    def __init__(self, hass, client, catalog, store, scan_interval):
        super().__init__(
            hass,
            LOGGER,
            name=DOMAIN,
            update_interval=timedelta(seconds=scan_interval),
            always_update=False,
        )
        self.client = client
        self.catalog = catalog
        self.store = store

    async def _async_update_data(self) -> AlcoholData:
        stock = await self.client.async_get_stock()
        plans = await self.hass.async_add_executor_job(
            plan_catalog, self.catalog, stock
        )
        return build_coordinator_data(stock, plans)

Use always_update=False only when the returned data objects have meaningful equality. Load and validate bundled JSON outside the event loop if it does significant synchronous work. At setup, save the coordinator in entry.runtime_data, perform the first refresh, and then forward the config entry to the sensor, button, and to-do platforms.

Useful coordinator output includes:

  • Available and unavailable recipes.
  • Bottle inventory normalized to milliliters.
  • Recipe plans and exact allocations.
  • Favorite and frequently made drinks.
  • Shopping suggestions.
  • A catalog revision and stock fingerprint for caching.

Cache plans against the stock that can actually affect them. Clear the cache whenever the catalog, substitutions, options, or relevant stock changes.

Treat consumption as a transaction, even when Grocy cannot

The user can choose a recipe and, where several products work, select which bottle to use. The action handler must not trust an old dashboard plan.

Use this sequence:

  1. Fetch live stock again.
  2. Re-plan with any explicit product selections.
  3. Reject the action if the full drink is no longer available.
  4. Collapse allocations by Grocy product ID.
  5. Convert milliliters back into each product’s Grocy stock units.
  6. Preflight every deduction.
  7. Send the Grocy consumption requests.
  8. Record history and request a coordinator refresh.

Grocy consumes each product in a separate request, so a multi-ingredient drink is not a true atomic database transaction. If one request succeeds and the next response is uncertain, re-read live stock before retrying. A blind retry can double-deduct the first ingredient.

Also support a separate neat-pour action. It should accept a positive ounce or milliliter quantity, verify that the product is an allowed spirit, confirm sufficient live stock, and deduct only that product.

Expose actions, not giant writable sensors

Home Assistant now calls service calls actions in the user interface. Register the integration’s actions in async_setup, not only when a config entry loads, so automations can validate them consistently. Define their fields in services.yaml. The developer action guide explains current registration and response-data rules.

An Alcohol Assistant can expose:

alcohol_assistant.consume_drink:
  recipe_id: simple-whiskey-sour
  servings: 1
  product_selections: {}

alcohol_assistant.consume_pour:
  product_id: 42
  amount_oz: 2

alcohol_assistant.set_favorite:
  recipe_id: simple-whiskey-sour
  favorite: true

alcohol_assistant.rate_drink:
  recipe_id: simple-whiskey-sour
  rating: 4

alcohol_assistant.refresh: {}

Validate every action with a schema. Keep user substitutions, favorites, ratings, and consumption history in Home Assistant’s persistent Store; never hide state inside entity attributes and hope it survives a restart.

Sensors should report small, automation-friendly state:

  • sensor.alcohol_assistant_available_drinks: number available.
  • sensor.alcohol_assistant_neat_pours: number of stocked spirits.
  • sensor.alcohol_assistant_bar_planner: number of unavailable favorites.
  • A read-only to-do entity for shopping suggestions, if useful.

Avoid putting the entire recipe catalog and all plans into sensor attributes. Home Assistant records state and attributes, so a huge menu creates database churn. Small catalogs can tolerate compact attributes; larger ones should use authenticated response actions or JSON views.

Serve large menus through authenticated views

For a rich local card, register a HomeAssistantView with requires_auth = True:

class AvailableDrinksView(HomeAssistantView):
    requires_auth = True
    name = "api:alcohol_assistant:available"
    url = "/api/alcohol_assistant/available"

    def __init__(self, coordinator):
        self.coordinator = coordinator

    async def get(self, request):
        return self.json(
            serialize_available(self.coordinator.data, request.query)
        )

Add pagination and cap the page size. Return only JSON-serializable data. Cache serialized menu data by catalog revision and stock fingerprint rather than rebuilding it for every card render.

Useful read endpoints are:

/api/alcohol_assistant/available
/api/alcohol_assistant/planner
/api/alcohol_assistant/bottles

The browser should call them through Home Assistant’s authenticated API helper, not by embedding a long-lived token in JavaScript.

Add a local dashboard card

Serve one JavaScript module from the integration, for example:

/alcohol_assistant/alcohol-assistant-card.js

Register the static path during setup, then add it as a JavaScript module under Settings → Dashboards → Resources. Home Assistant documents the supported resource mechanisms in Registering resources.

One module can register several cards:

customElements.define("alcohol-assistant-card", AlcoholAssistantCard);
customElements.define("alcohol-assistant-neat-menu-card", NeatMenuCard);
customElements.define("alcohol-assistant-planner-card", BarPlannerCard);

Example dashboard configuration:

type: custom:alcohol-assistant-card
title: What can I make?
show_search: true
confirm_before_consuming: true

The card should:

  • Read large data with Home Assistant’s authenticated API helper.
  • Call actions with hass.callService or the current WebSocket action API.
  • Confirm before deducting stock.
  • Escape recipe names, product names, and all other catalog text before inserting HTML.
  • Re-render on relevant entity changes, not on an aggressive timer.
  • Provide a card editor and a stub configuration.
  • Remain useful on a phone-sized dashboard.

Whatever the card sends you is just a hint about what the user wants - don’t let it decide anything on its own. The backend still performs the live re-plan and validation.

Set up the entry cleanly

The integration lifecycle should be predictable:

PLATFORMS = [Platform.SENSOR, Platform.BUTTON, Platform.TODO]

async def async_setup_entry(hass, entry):
    client = GrocyClient(
        async_get_clientsession(hass),
        entry.data[CONF_URL],
        entry.data[CONF_API_KEY],
    )
    catalog = await hass.async_add_executor_job(load_catalog, hass.config.path())
    coordinator = AlcoholCoordinator(hass, client, catalog, store, scan_interval(entry))
    await coordinator.async_config_entry_first_refresh()
    entry.runtime_data = coordinator
    await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
    return True

Add an options-update listener that reloads the entry when scan interval or planning behavior changes. On unload, unload every forwarded platform and unregister entry-specific views or listeners. Global actions and a global static resource should be registered once, not once per reload.

Test the dangerous parts first

Do not test consumption for the first time against irreplaceable inventory. Use a development Grocy database or disposable products.

At minimum, cover these cases:

Catalog tests

  • Missing parent, cycle, duplicate ID, and invalid regular expression.
  • Recipe references an unknown ingredient.
  • Substitution references an unknown node.
  • Custom recipe cannot silently overwrite a bundled recipe.

Engine tests

  • Exact matches beat substitutions.
  • Specific descendants beat generic ancestors.
  • Substitution rank and fidelity are respected.
  • Two ingredients cannot spend the same stock twice.
  • User product selections are honored or rejected clearly.
  • Non-depleting ingredients never create Grocy deductions.

API tests

  • Invalid key, unreachable server, malformed response, and timeout.
  • Bottle-count-to-milliliter conversion.
  • Unit rounding does not overdraw stock.
  • Ambiguous grocery products remain unclassified.
  • A failed consumption response causes a live reread before any retry.

Home Assistant tests

  • Config flow success, duplicate server, cannot-connect, invalid-auth, and reauth.
  • First-refresh failure raises a useful setup error.
  • Sensors become unavailable during coordinator failure without losing prior data.
  • Action schemas reject invalid quantities and unknown recipes.
  • The HTTP views require authentication and enforce pagination limits.
  • The card renders empty, unavailable, loading, and error states.

Use Home Assistant’s Integration Quality Scale as a release checklist even though a personal custom integration is not part of Home Assistant Core.

Install it manually first

For development:

  1. Copy alcohol_assistant into <config>/custom_components/.
  2. Restart Home Assistant.
  3. Add Alcohol Assistant from Settings → Devices & services.
  4. Enter the local Grocy URL and API key.
  5. Add the card JavaScript module under dashboard resources.
  6. Add the custom card to a test dashboard.
  7. Create a disposable Grocy product and verify a small pour end to end.

Watch both Home Assistant and Grocy logs. Confirm the displayed quantity, planned deduction, Grocy request unit, and final live stock all agree.

Package it for HACS

HACS is convenient once manual installation is stable. Its integration publishing rules expect integration files under custom_components/<domain> and currently allow one integration per repository.

That last rule matters. If Alcohol Assistant and Meal Assistant live in one development monorepo, split them into separate release repositories before submitting them to HACS. Do not point HACS at a repository containing both custom integrations and assume it will choose correctly.

For a public default listing, follow the current HACS inclusion requirements, including repository validation, releases, the HACS validation action, and Home Assistant’s hassfest checks. For personal use, a custom repository is enough, but it should still have tagged releases and a clear changelog.

Common mistakes

  • Matching only by product name. Add explicit canonical markers and capacities to Grocy products.
  • Treating categories as a flat list. Use a graph with cached ancestry.
  • Encoding substitutions inside every recipe. Maintain one ranked substitution graph.
  • Checking ingredients independently. Allocate the entire recipe against one temporary stock ledger.
  • Trusting a stale dashboard. Fetch and re-plan immediately before consumption.
  • Retrying uncertain writes. Re-read Grocy first to avoid double deductions.
  • Storing the whole catalog in sensor attributes. Use small states and authenticated large-data endpoints.
  • Blocking Home Assistant’s event loop. Keep network calls asynchronous and move meaningful synchronous catalog work to the executor.
  • Putting secrets in source code or JavaScript. Store the API key in the config entry and use Home Assistant authentication in the browser.
  • Publishing two integrations in one HACS repository. Use one release repository per integration.

Where to take it next

Once the core is trustworthy, useful additions include nonalcoholic recipe modes, expiration-aware mixer suggestions, per-person favorites, barcode-assisted bottle setup, consumption statistics, and an Assist conversation layer that calls the same validated actions.

Every new interface has to sit on top of the same conservative engine. A voice command, a dashboard tap, or an AI skill can start the request, but the live stock validation and the exact deductions always happen in one backend path.

Get that right and you’ve got something you can trust at the end of a long day.

The food side of the house runs on the same pattern - if you want to plan meals against your pantry instead of drinks against your bar, see how to build a local Meal Assistant. And rather than typing every bottle in by hand, I stock the bar by pointing a camera at it, which is a separate photo-intake skill. Both sit on the Echo Show display build if you want to see where they live.

Keep going

All articles →
Article

How to Build a Local Meal Assistant Integration for Home Assistant

Build a Home Assistant custom integration that reads Grocy stock, plans meals with controlled substitutions, and safely deducts the ingredients you cook.

Read article →
Article

Build Local AI Skills to Load Grocy From Photos

How to build Codex skills that read bottle, grocery, receipt, and meal photos and safely update a local Grocy inventory, with optional Home Assistant refreshes.

Read article →
Article

Best Zigbee leak sensors with shutoff integration

Real Zigbee water leak sensors paired with Zigbee/Z-Wave shutoff valves for fully local leak protection in Home Assistant.

Read article →
New guides and product notes, no inbox required. Follow via RSS →