Skip to content

Instantly share code, notes, and snippets.

@mpapierski
Last active September 16, 2026 12:19
Show Gist options
  • Select an option

  • Save mpapierski/7a2e9b19ee8c11dba35a65455050cd57 to your computer and use it in GitHub Desktop.

Select an option

Save mpapierski/7a2e9b19ee8c11dba35a65455050cd57 to your computer and use it in GitHub Desktop.
esp-coffee-bridge AI agent skill
name coffee-bridge-barista
description Operate the NIVONA coffee bridge over its HTTP API, including durable brew queues, API v2 BLE jobs, cache-backed resources, settings, histories, backups, and maintenance.

Coffee Bridge Barista

Use this skill to inspect, control, or automate a local NIVONA coffee bridge. Prefer saved-machine endpoints over low-level BLE, GATT, or protocol diagnostics.

Safety and truthful reporting

  • Check GET /api/status and GET /api/machines before machine-specific work.
  • Treat brewing, confirmation, setting/recipe writes, deletion, restore, OTA, and reboot as mutations. Perform only the mutation requested.
  • Never resubmit a mutation after a timeout, disconnect, restart, or ambiguous response. Recover its existing job or brew by ID.
  • Distinguish commandAccepted from physical completion. Report a drink as completed only when its brew state is completed.
  • Surface attention_required with the machine's message, such as refill water, fill beans, empty trays, move cup/open valve, or flush.
  • Never send HY automatically. Use the confirmation endpoint only when the user asks or explicitly agrees to the machine prompt.
  • reconnecting is not failure. The bridge keeps observing indefinitely.
  • unknown, failed, and interrupted pause the queue. Never continue past an unverified outcome without explicit user acknowledgement.
  • For one drink, do not mention cup sensing or ask the user to confirm a cup. Treat the brew request itself as readiness to dispense.
  • For a batch that needs separate cups, make cup changes an actionable workflow: set holdAfter: true on every drink except the last, and ask the user to replace the cup only when each completed hold is reached. Do not recite the bridge's hardware limitations as a generic warning.
  • Back up and validate durable data before restore, bulk deletion, or firmware work.

Detect the current API

Start with GET /api/status. The current local API reports:

{
  "apiVersion": 2,
  "capabilities": {
    "asyncBleJobs": true,
    "websocketEvents": true,
    "eventProtocolVersion": 2,
    "durableBrewQueue": true,
    "eventsUrl": "/api/events"
  }
}

Unversioned paths are used. Generic BLE operations return boot-scoped API v2 jobs. Brews are different: they return durable brew records and survive reboot/OTA.

Resolve generic API v2 jobs

When a non-brew operation returns 202 with job.id:

  1. Retain the job ID and do not resubmit the request.
  2. Poll GET /api/jobs/{id} after pollAfterMs, or watch it over WebSocket.
  3. On succeeded, follow the exact same-origin job.resultUrl.
  4. On failed or cancelled, surface job.error; do not retry a mutation.
  5. Bound the whole operation to about 60 seconds unless the user asks otherwise.

Jobs are boot-scoped and expire after five minutes. Valid states are queued, running, succeeded, failed, and cancelled.

For WebSockets, connect to /api/events, require event protocol 2, use watch_job/unwatch_job for generic jobs, and accept broadcast brew events. After reconnect or resync, reload GET /api/brew-queue and recover generic job watches. REST is authoritative.

Cache-backed resources and implicit sessions

The bridge owns BLE/HU sessions. A first saved-machine access establishes the session implicitly; clients do not call a separate online endpoint. A failed session is marked offline, and later access reconnects.

  • summary: 60-second cache
  • stats and settings: 15-minute cache
  • features: 24-hour cache

Fresh/stale responses return 200; cold or ?refresh=1 reads can return a generic job. Stale data remains usable while refresh runs. Recipe and MyCoffee details use persistent LittleFS caches.

Recommended workflow

  1. Read status and machines; select a saved machine by serial.
  2. Read /api/machines/{serial}/summary for context. Offline status does not prohibit enqueueing: the bridge will connect when the brew reaches the head.
  3. Read /recipes, then /recipes/{selector} for a customized drink.
  4. Use only writableFields and listed option codes; never guess model limits.
  5. Enqueue the brew with a fresh persistent correlationId.
  6. Retain brew.id and follow /api/brews/{id} until a terminal outcome or user intervention is required.

Enqueue and follow brews

POST /api/machines/{serial}/brew requires a caller-generated printable 1-64-character correlationId. Reuse the same ID only for an exact retry of the same canonical request. The bridge deduplicates it; different content returns 409 correlation_conflict.

{
  "selector": 2,
  "strengthBeans": 3,
  "aroma": 2,
  "temperature": 2,
  "coffeeAmountMl": 120,
  "source": "ai",
  "actor": "Barista Agent",
  "label": "Bright Morning Lungo",
  "note": "Softer morning cup.",
  "correlationId": "barista-20260912-001",
  "holdAfter": false
}

The response is 202 with brew.id, brew.state, and Location: /api/brews/{id}. Do not pass this response to the generic job resolver.

Valid states:

  • queued, waiting_for_machine, dispatching
  • accepted — the machine acknowledged the command; the drink is not done
  • preparing
  • attention_required — report machineStatus.messageLabel and wait
  • reconnecting — observation is temporarily unavailable; keep waiting
  • completed — dispensing was observed and the machine stably returned ready
  • failed, interrupted, unknown, cancelled

The queue is global FIFO with 16 slots. By default it advances after verified completion. For a multi-cup batch, set holdAfter: true on every drink except the last. When a held drink reaches completed, ask the user to replace the cup; after they confirm, continue it with POST /api/brews/{brewId}/resolve and {"action":"continue"}. If the user explicitly says one container can hold the whole batch, it is fine to leave holdAfter false. The queue accepts an offline machine and waits indefinitely. Before sending the command, it requires a fresh ready observation, so it also waits behind a manual/front-panel drink, cleaning, startup, or an operator prompt.

If GET /api/brew-queue reports recoveryBlocked: true, do not enqueue or try to bypass the queue. Automatic dispatch is fail-closed until its stored state is repaired.

Management routes:

  • GET /api/brew-queue
  • GET /api/brews/{brewId}
  • DELETE /api/brews/{brewId} for a definitely unsent item
  • DELETE /api/brew-queue to cancel every definitely unsent item
  • POST /api/brews/{brewId}/resolve

Resolution bodies are {"action":"abandon"} to stop monitoring and mark an active outcome unknown, {"action":"cancel_remaining"} to preserve the head and cancel the unsent tail, or {"action":"continue","acknowledgeRisk":true} to advance after an ambiguous terminal outcome. Ask the user before the risk acknowledgement.

Machine prompts and confirmation

Known messages take precedence over the process code. Refill-water, beans, trays, brewing-unit, powder-shaft, move-cup, and flush states are not brew completion. The bridge keeps the brew in attention_required and resumes observation after the condition clears.

Use POST /api/machines/{serial}/confirm only when the current status reports hostConfirmSuggested: true and the user wants to proceed. It is a generic asynchronous mutation, not a brew resolution action.

History, backup, and maintenance

Brew history contains one finalized record per brew with brewId, correlation, command-acceptance flags, physical outcome, raw final status, completion evidence, applied recipe, and timestamps. Counter history is separate.

The live queue is intentionally excluded from backup; it persists independently across reboot/OTA. The bridge rejects saved-machine deletion/reset, backup restore, and destructive brew-history cleanup while affected brews are unresolved.

Before OTA, download and validate /api/backup/export, archive a crash dump if present, then upload firmware. Never trigger a real brew merely to validate a deployment unless the user separately authorizes it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment