This document defines the Home Assistant entity model for Phase 1. It is derived from
the observed CodexBar v0.55.1 contract. The bridge consumes only the
bare, cached GET /usage response from a loopback-bound codexbar serve process.
Publish one Home Assistant device for the telemetry bridge on the collector host. All
provider/window entities belong to that device. Provider and window identifiers form the
stable part of each entity's MQTT discovery unique_id; display labels are presentation
only and may change without creating a new entity.
The entity set must be generated from each response. Do not assume that every provider has session and weekly windows, and do not hardcode the observed Antigravity bucket list.
For every healthy provider row:
- Read every member of
usage.extraRateWindows[]and use itsidas the stable bucket key. The human-facingtitleis only the label. - When no detailed extra-window set represents the provider's quota buckets, read each
non-null named window under
usage(primary,secondary, and any future window-shaped member supported by the normaliser). Do not duplicateprimaryorsecondaryaliases when the detailed extra-window set already represents them. - Keep the selected source bucket key in the entity identity. Do not rename a
primarywindow tosession, or asecondarywindow toweekly, based only onwindowMinutes. - Emit a remaining-percent entity and a reset-timestamp entity for each bucket.
This distinction is necessary because the observed Antigravity primary and secondary
windows are both 10,080 minutes, and its actual quota detail is in dynamically named
extraRateWindows[] buckets such as antigravity-quota-summary-gemini-5h.
| Entity | State | Home Assistant metadata | Attributes |
|---|---|---|---|
| Remaining quota | 100 - usedPercent |
device_class: battery; unit_of_measurement: "%"; state_class: measurement |
Provider, source bucket key, windowMinutes, dataConfidence, and the pace fields described below when present |
| Reset time | resetsAt |
device_class: timestamp |
Provider, source bucket key, windowMinutes; resetDescription may be included as explanatory text |
Preserve the numeric precision supplied by CodexBar unless Home Assistant requires a
presentation rounding rule. Clamp only to defend against invalid upstream values; do not
silently convert a missing usedPercent into zero.
The timestamp state is the ISO 8601 value from resetsAt. A bucket without resetsAt
must not publish a fabricated timestamp.
For named buckets, match pace.<bucket-key> to usage.<bucket-key>. When a pace block is
present, attach these fields to the remaining-quota entity:
stagedeltaPercentexpectedUsedPercentwillLastToResetetaSecondssummary
Fields are independently optional. Omit missing fields rather than supplying false, zero, or empty defaults. The observed Antigravity row has no pace block, which is a valid provider shape and must not make its quota entities unavailable. No pace-to-extra-window mapping is defined by the observed contract, so do not infer one.
Expose the row's usage.dataConfidence on every remaining-quota entity for that provider.
Consumers must be able to distinguish observed exact data from Claude's observed
percentOnly data. A missing value remains unknown; do not promote it to exact.
The fixture captured on 2026-08-27 records Claude as percentOnly. Antigravity is healthy
through source=cli after the credential remediation described in the
operator runbook; its lack of pace remains expected.
When usage.codexResetCredits is present, publish a provider-level numeric entity from
availableCount. It is not a quota-window entity and must not be duplicated once per
bucket. The observed Codex fixture contains one available reset credit.
Useful non-secret attributes may include the upstream updatedAt. Do not publish OAuth
material or any credential-store content as entity state or attributes.
Treat an error object on one provider row as a row-level failure. Continue normalising
and publishing every healthy row even if the HTTP response is non-200 or another provider
failed. Do not clear healthy entities because one provider failed.
For a failed provider row, retain the last known quota state rather than replacing it with zero or an invented unavailable state. Error diagnostics may be logged, but must be sanitised and must never include tokens.
Retain both Home Assistant discovery messages and state messages. This allows the last known reading to survive a collector restart. After a broker restart, state must be restored either from the broker's persisted retained store or from the collector's full retained republish on reconnect.
The bridge owns only the exact discovery, state, and attributes topics recorded in its private retained inventory manifest. That manifest is scoped by the configured MQTT topic prefix and collector host identity; it is not a Home Assistant discovery entity. After a complete successful source response with no provider errors, the bridge publishes the complete desired topic map, tombstones obsolete manifest entries with an empty retained QoS 1 payload, and writes the replacement manifest last. A failed publish leaves the previous manifest authoritative so reconciliation can be retried safely.
Partial and error snapshots do not perform deletion or advance the manifest. Healthy rows in such a snapshot may refresh their existing values, but a failed or omitted provider's last-known topics remain retained. The manifest governs topics published after this lifecycle was introduced; historical pre-manifest topics require an explicit operator cleanup rather than broad broker-wide adoption.
Use a generous expire_after or omit it. Do not derive entity availability from
CodexBar's staleAfterSeconds: it is 900 under serve, describes the upstream snapshot,
and is not the Home Assistant entity lifetime. A stale retained value is intentionally
preferred to an unavailable ambient display in Phase 1.
Phase 1 is complete only when the retained state round-trip has been tested with real authenticated broker credentials and the entities survive both collector and broker restarts. TCP reachability to port 1883 alone does not satisfy this requirement.