01

The Problem

I run Grocy for household stock management — pantry items, cleaning supplies, anything consumable that I want to track. Every product has a quantity, a storage location, and optionally an expiry date. It’s all there. Grocy’s web interface is fine, but opening a browser to check whether I have olive oil, and how many bottles, and whether any of them are about to expire, is exactly the kind of friction that makes you stop maintaining the system.

What I wanted was simpler: tap a shortcut on my phone, type a name, and have the answer spoken aloud in the room I’m standing in — with the expiry date included if one’s been logged.

The friction of opening a browser to answer a kitchen question is exactly what kills home inventory habits. Remove the friction and the system actually gets used.

Everything I needed was already running. Grocy on my home server. Home Assistant on a dedicated device. Tasker on my phone. Sonos in the living room. The only thing missing was a small piece of middleware to tie them together.

02

The Pipeline

The full path from question to spoken answer looks like this:

Pipeline — Grocy Voice Query
Tasker widget
S23 Ultra
Input dialog
search term
Flask :8770
home server
Grocy API
stock lookup
Piper TTS → Sonos
Phone notification

Nothing in that chain is cloud-dependent. No third-party subscriptions. The whole thing runs on hardware I own and software I control.

03

The Pieces

Each component in the pipeline has a specific role. Here’s the full stack and what each part contributes.

Stock Manager
Grocy
Self-hosted household stock app. Tracks products with quantity, location, and expiry date. Exposes a REST API with a static key — no login flow required.
Home Server
Linux Home Lab
Ubuntu machine on the local network. Runs the Flask middleware service as a systemd unit. Always on, no manual restarts.
Phone Automation
Tasker
Android automation on Samsung S23 Ultra. Handles the widget shortcut, dialog input, HTTP request, and reprompt loop.
Smart Home Hub
Home Assistant
Coordinates voice output and pushes the phone notification. The server calls HA directly — no Tasker response parsing needed.
Voice Output
Piper TTS + Sonos
Piper renders the response text to speech locally. Sonos plays it in the room. No cloud TTS services involved.
Remote Access
Tailscale
Mesh VPN. The pipeline works identically from the driveway as it does from the kitchen. One address, every location.
04

The Auth Model

Grocy’s API authentication is different from most self-hosted apps and worth understanding before you write a line of code. There’s no login endpoint, no token exchange, no session cookie. Grocy uses a static API key passed as a request header on every call.

Every request includes GROCY-API-KEY: your_key_here in the header. That’s the entire auth model. You generate the key once in Grocy’s settings, store it in your service config, and reference it on every API call. No token caching, no refresh logic, no expiry to manage.

// TIP
Find your API key under Grocy → Settings → Manage API keys. Generate one, copy it into your service config, and you’re done. The key doesn’t rotate unless you explicitly revoke it.
05

The Search Problem

Grocy’s API has a query filter syntax that looks like it should work for product search: query[]=name~like~{term}. Pass a search term, get back matching products. Except on a number of Grocy versions, this filter operator is either unsupported or silently broken — it returns an empty array regardless of what you search for.

I confirmed this by running a full product dump against my instance: the products were there, the search syntax just didn’t find them. The ~like~ operator isn’t reliably implemented across Grocy versions.

// NOTE
Don’t use the Grocy query filter for product name search. It appears functional in the API docs but behaves inconsistently across versions. The fix below is version-safe and doesn’t depend on server-side filtering at all.

The fix is straightforward: pull the full product list from /api/objects/products and filter on the Python side using a simple substring match. It’s one extra network call for a dataset that’s never more than a few hundred items — the performance difference is immeasurable, and the behavior is completely predictable regardless of Grocy version.

06

How It Works

The Tasker widget fires a task that opens a native input dialog on the phone. Type what you’re looking for, tap OK. The task packages the input as a JSON payload and sends it to the Flask service running on the home server.

The Flask service receives the search term, pulls the full Grocy product list, filters by name, and then fetches stock detail for each match. Stock detail comes back from a single endpoint — quantity, opened count, location, and expiry date are all in one response. No chained calls needed.

Input
Tasker widget + dialog

Tap the shortcut on the phone. Type a product name. That’s the entire user interaction before the answer comes back.

Transit
HTTP POST to Flask on the server

Tasker sends {“item”: “search_term”} to the Flask service on port 8770. Works over LAN or Tailscale from anywhere.

Lookup
Full product list pull + Python filter

Flask fetches all products from Grocy, filters by name substring match, then pulls stock detail for each matching product.

Response
Tiered result logic with expiry

One match gets full spoken detail. Multiple matches get listed. Too many triggers a reprompt. Expiry dates are spoken if present and valid.

Output
Sonos TTS + phone notification simultaneously

The server calls Home Assistant directly for both outputs. Piper renders the text to speech locally and plays it on Sonos. HA pushes a persistent tray notification.

07

The Expiry Layer

Expiry date was the feature that made this more than a stock count tool. Grocy stores next_due_date in the stock detail response — but it requires careful handling before you can safely speak it.

Grocy uses the sentinel value 2999-12-31 to indicate that no expiry has been set for a product. That date needs to be caught and filtered before it reaches the TTS engine — “expires December 31st, 2999” is not a useful response. Any date matching the sentinel is treated as no expiry and omitted from the spoken output entirely.

For real dates, the logic branches on whether the date is in the past or future. Past due dates are spoken as “expired” rather than reading a date string. Future dates are formatted as a natural month and day with an ordinal suffix — “expires June 3rd” rather than “expires 2026-06-03”. That one formatting decision makes the response sound intentional rather than machine-generated.

// NOTE
Grocy’s next_due_date sentinel is 2999-12-31. Always filter this value before any date processing. Products without an expiry date set will carry this value — it’s not an error, it’s the default.
08

What It Sounds Like

The spoken response varies based on what the search returns. The format is consistent, and every piece of available stock information is included in the right order:

1 match
“You have 1 Package of Aluminum Foil, 1 opened, in the Drawer, expires June 3rd.”
2–4 matches
“Grocy shows: 2 Bottles of Olive Oil, in the Pantry; 1 Bottle of Olive Oil, expired, in the Garage.”
4+ matches
Dialog re-opens automatically. Narrow your search.
No matches
“Nothing found for [term] in Grocy.”
No expiry
“You have 3 Rolls of Paper Towels in the Utility Closet.” (expiry omitted)
09

The Reprompt Loop

A broad search term can return dozens of matches. A long list read aloud by Piper isn’t useful, and TTS engines will silently drop responses that exceed a length threshold anyway. The solution is to treat “too many results” as a distinct state rather than an edge case to flatten.

When match count exceeds four, the server returns HTTP 300 with a brief message instead of a full spoken response. Tasker reads %http_response_code, recognizes 300 as the refinement signal, and re-opens the input dialog. From the user’s perspective the dialog just reappears — there’s no error state, no failure message, just a second prompt.

// NOTE
Tasker’s Collision Handling must be set to Run Both Together. The reprompt loop works by having the task call itself. The default Abort New Task setting will silently kill the second invocation and break the loop with no error.
10

The Interesting Problems

Aside from the API filter issue already covered, a few things came up that are worth documenting for anyone building something similar.

01
Grocy’s query filter syntax is unreliable across versions. The ~like~ operator documented in the API returns empty results on some installs. Pulling the full product list and filtering in Python takes milliseconds and works on every version. Don’t waste time debugging the server-side filter — just move the logic to your service.
02
Stock detail lives in a single endpoint. /api/stock/products/{id} returns quantity, opened count, location, and expiry date in one response. No second API call needed. The endpoint returns 400 for products that have never had any stock logged — handle that case explicitly or you’ll get unhandled exceptions on valid products.
03
The sentinel date needs to be caught before any date logic runs. 2999-12-31 will pass every validity check — it’s a perfectly valid date. The filter has to happen first, before any comparison to today’s date, before any formatting. Filter by sentinel value, not by date range.
04
Ordinal suffix formatting is worth the three lines. “Expires June 3rd” sounds intentional. “Expires 2026-06-03” sounds like a bug. The suffix logic handles 11th/12th/13th correctly — those are the only edge cases where the regular st/nd/rd/th pattern breaks.
05
Have the server push the notification, not Tasker. Tasker’s HTTP response body variables don’t resolve reliably in this version. Rather than fighting that, the Flask service calls Home Assistant directly — HA pushes the notification to the phone tray. The result is the same, the path is cleaner, and there’s nothing to debug in Tasker’s response handling.
11

The Flask Service

The middleware is a single Python file running as a systemd service. It stays alive permanently — no manual starts, no cron hacks, automatic restart on failure. The service listens on port 8770 for POST requests to /grocy/query.

The core flow: receive the search term, pull all Grocy products, filter by name, fetch stock detail for each match, build the response string with quantity, opened count, location, and expiry, then fire Piper TTS via Home Assistant and push the phone notification — all before returning the HTTP response to Tasker.

The Tasker task itself was cloned directly from an existing inventory query task. Three fields changed: the variable name, the JSON body variable, and the endpoint URL. Everything else — the dialog setup, the reprompt loop logic, the collision handling setting — carried over unchanged.

12

Why This Works as a System

The value isn’t the individual pieces — Grocy, Home Assistant, and Tasker all have decent interfaces on their own. The value is in connecting them so that the friction of answering a practical question drops below the threshold where you’d bother.

Grocy is only useful if you keep it updated. You keep it updated if looking things up is easy. If looking things up requires opening a browser, navigating to the app, and finding the product, you’ll start skipping updates because the system “isn’t worth maintaining.” The pipeline removes that excuse entirely.

This is the same architecture as the inventory query pipeline built before this one. Same Flask pattern, same Tasker task structure, same HA notification path. Different endpoint, different API auth model, one new data field. The work was in understanding Grocy — the plumbing was already proven.

If you’re running Grocy and want to build something similar, everything here is open-source or free: Grocy, Home Assistant, Tasker, Piper TTS, and a Linux server. The architecture is straightforward. The tricky parts are the API filter behavior and the expiry sentinel — both documented above so you don’t have to rediscover them.

Filed under: Home Lab / Home Automation — technococo.com

Dedicated to bringing you breaking news, insightful analysis, and expert opinions in the world of technology. From the latest news to education, we strive to keep you informed about the ever-changing tech scene.

You May Have Missed