A Home Assistant custom integration for managing household inventories. Track items across multiple inventories with expiration dates, automatic todo list integration, barcode support, locations, categories, and change history.
or:
- Add this repository to HACS
- Install "Simple Inventory"
- Install the companion card: Simple Inventory Card
- Restart Home Assistant
- Copy
custom_components/simple_inventory/to your Home Assistantcustom_components/directory - Restart Home Assistant
This integration works best with the companion Lovelace card: Simple Inventory Card
Add via Home Assistant UI: Settings -> Devices & Services -> Add Integration -> Simple Inventory
When you create your first inventory, a global device is also created automatically. Each inventory becomes a device with two sensors.
You can edit inventory names, icons, and descriptions in the integration options flow after creation.
Each item has a name (required) and these optional fields:
| Field | Description |
|---|---|
quantity |
Current stock level (supports decimals, e.g. 2.5) |
unit |
Unit of measurement (e.g. "boxes", "L", "kg") |
description |
Free-text description |
barcode |
UPC/EAN barcode for scanning |
location |
Where the item is stored (supports multiple, comma-separated) |
category |
Item category (supports multiple, comma-separated) |
price |
Current unit price (updated when restocking or editing) |
expiry_date |
Expiration date (YYYY-MM-DD) |
expiry_alert_days |
Days before expiry to trigger alerts |
Items can belong to multiple locations and categories. Pass comma-separated values:
location: "Pantry, Kitchen"
category: "Snacks, Bulk Items"The API returns both scalar fields (location, category with the first value) and array fields (locations, categories with all values) for backward compatibility.
Set an expiry_date and expiry_alert_days on any item. When the number of remaining days is at or below your threshold, the item appears on the expiry sensors. Items with zero quantity are excluded from expiry alerts.
Automatically add items to a Home Assistant todo list when stock drops below a threshold. Configure these fields:
| Field | Description |
|---|---|
auto_add_enabled |
Enable/disable auto-add for this item |
auto_add_to_list_quantity |
Quantity threshold that triggers adding to the list |
desired_quantity |
Target stock level (controls how much to buy) |
todo_list |
Target todo list entity (e.g. todo.shopping_list) |
todo_quantity_placement |
Where to show the needed quantity: "name" (e.g. "Milk (x4)"), "description", or "none" |
How it works:
- When
quantitydrops to or belowauto_add_to_list_quantity, the item is added to your todo list - When
desired_quantity > 0(fixed mode): the todo shows the desired quantity, and the item is removed from the list whenquantity >= desired_quantity - When
desired_quantity = 0(threshold mode): the todo showsthreshold - quantity + 1, updates live on each change, and the item is removed whenquantity > threshold
Note: The built-in todo.shopping_list does not support item descriptions, so description-based features only work with other todo list integrations.
Enable auto_add_id_to_description_enabled to append the inventory ID to item descriptions. This is useful when scanning barcodes from todo lists — the ID lets automations route the item back to the correct inventory. See this issue for the use case.
Items can have multiple barcodes associated with them (comma-separated in the API). Most service calls accept either name or barcode to identify an item. Two dedicated barcode services make scanning workflows easy:
lookup_by_barcode— Search for an item by barcode across all inventoriesscan_barcode— Scan a barcode and perform an action (increment, decrement, or lookup) with automatic cross-inventory resolution
Important: Barcodes with leading zeros (e.g.
0123456) must be quoted in YAML automations and scripts. Unquoted values likebarcode: 0123456are interpreted as integers by YAML, stripping the leading zero and matching the wrong item. Always usebarcode: "0123456". This does not affect the HA service call UI or the WebSocket API, which handle strings correctly.
Every add, remove, increment, and decrement is recorded with before/after quantities and timestamps. Query history via the WebSocket API.
Track how fast you consume items. The integration calculates consumption rates based on decrement history:
- Daily / weekly consumption rate — How much you use per day or week
- Days until depletion — Estimated days before the item runs out at the current rate
- Average restock interval — How often you typically restock the item
- Total consumed / events tracked — Lifetime consumption totals
Rates can be calculated over a configurable time window (e.g. last 30, 60, or 90 days) or across all history. The companion card shows this data in a "Consumption" tab in the item history modal. You can also query rates via the service call or WebSocket API for use in automations.
Track item prices to see inventory value and spending trends. Each item has an optional price field representing the current unit price.
- Unit price: Set when adding or editing an item. Update it whenever the price changes (e.g. at the next purchase).
- Price on restock:
increment_item,decrement_item, andscan_barcodeaccept an optionalpriceparameter. When provided, it updates the item's stored unit price and records the price on the history event. - Total value: The inventory sensor includes a
total_valueattribute — the sum ofprice × quantityacross all items with a price set. Items withprice = 0are excluded. - Spend analytics: The consumption tab shows spend data computed from restocking (increment/add) events that have a price recorded:
- Daily Spend — Average daily purchasing cost over the observation window
- Weekly Spend — Average weekly purchasing cost
- Total Spend — Total money spent purchasing the item
Note: A price of 0 means "no price set", not "free". Items with no price are excluded from value and spend calculations.
The integration fires Home Assistant events on key inventory transitions, enabling automations without polling sensor state.
| Event | When it fires |
|---|---|
simple_inventory_item_added |
A new item is added to an inventory |
simple_inventory_item_removed |
An item is deleted from an inventory |
simple_inventory_item_quantity_changed |
Any increment or decrement |
simple_inventory_item_depleted |
Quantity drops to 0 (was > 0) |
simple_inventory_item_restocked |
Quantity rises above 0 (was 0) |
simple_inventory_item_added_to_list |
Item is newly added to a todo list |
simple_inventory_item_removed_from_list |
Item is removed from a todo list |
Event payloads:
item_added: item_name, inventory_id, quantity
item_removed: item_name, inventory_id
item_quantity_changed: item_name, inventory_id, quantity_before, quantity_after, amount, direction ("increment" or "decrement")
item_depleted: item_name, inventory_id, previous_quantity
item_restocked: item_name, inventory_id, quantity
item_added_to_list: item_name, inventory_id, quantity, todo_list, quantity_needed
item_removed_from_list: item_name, inventory_id, quantity, todo_list
Export your inventory to JSON or CSV, and import data back with configurable merge strategies (skip, overwrite, or merge_quantities). Available via the WebSocket API.
Each inventory creates two sensors:
sensor.<name>_inventory — Main inventory sensor
- State: Total quantity across all items
- Attributes:
inventory_id— The config entry IDtotal_items— Number of distinct itemstotal_quantity— Sum of all quantitiestotal_value— Sum ofprice × quantityacross all priced itemscategories— Category names with item countslocations— Location names with item countsbelow_threshold— Items that need restocking (withquantity_needed)expiring_soon— Count of items expiring soonitems— Full list of all items with all fields
sensor.<name>_items_expiring_soon — Expiry alert sensor
- State: Count of expiring + expired items
- Icon: Changes dynamically (
mdi:calendar-removefor expired,mdi:calendar-alertfor expiring,mdi:calendar-checkwhen all clear) - Attributes:
expiring_items— List of items expiring soonexpired_items— List of already-expired itemstotal_expiring,total_expired— Counts
sensor.all_items_expiring_soon — Aggregates expiring items across ALL inventories
- State: Total count of expiring items across all inventories
- Icon: Progressive severity (
mdi:calendar-remove->mdi:calendar-alert->mdi:calendar-clock->mdi:calendar-week->mdi:calendar-check) - Attributes:
expiring_items,expired_items— Aggregated listsnext_expiring,oldest_expired— Soonest/oldest datesinventories_count— Number of inventories
All services are under the simple_inventory domain. You can find your inventory ID by going to Developer Tools -> States and filtering for "inventory" — the ID is shown in the sensor attributes.
Add a new item to an inventory.
service: simple_inventory.add_item
data:
inventory_id: "01JYFPCDMBRBRK4MB3C26S2FKH"
name: "Frozen Pizza"
quantity: 5
unit: "boxes"
category: "Frozen Foods"
location: "Basement Freezer"
price: 8.99
barcode: "012345678901"
expiry_date: "2026-06-15"
expiry_alert_days: 7
auto_add_enabled: true
auto_add_to_list_quantity: 2
desired_quantity: 5
todo_list: "todo.grocery_list"
todo_quantity_placement: "name"
description: "Family size pepperoni"Only inventory_id and name are required. All other fields are optional.
Remove an item. Specify either name or barcode.
service: simple_inventory.remove_item
data:
inventory_id: "01JYFPCDMBRBRK4MB3C26S2FKH"
name: "Frozen Pizza"Increase an item's quantity. Supports decimal amounts. Optionally pass price to update the item's unit price (e.g. when restocking at a new price).
service: simple_inventory.increment_item
data:
inventory_id: "01JYFPCDMBRBRK4MB3C26S2FKH"
name: "Frozen Pizza"
amount: 3
price: 9.49You can also use barcode instead of name:
service: simple_inventory.increment_item
data:
inventory_id: "01JYFPCDMBRBRK4MB3C26S2FKH"
barcode: "012345678901"
amount: 1Decrease an item's quantity. Same parameters as increment_item.
service: simple_inventory.decrement_item
data:
inventory_id: "01JYFPCDMBRBRK4MB3C26S2FKH"
name: "Frozen Pizza"
amount: 1Update any fields on an existing item. Use old_name to identify the item and name for the (possibly new) name.
service: simple_inventory.update_item
data:
inventory_id: "01JYFPCDMBRBRK4MB3C26S2FKH"
old_name: "Frozen Pizza"
name: "Frozen Pizza"
category: "Frozen Foods, Quick Meals"
location: "Kitchen Freezer"
expiry_date: "2026-08-01"Retrieve all items for a specific inventory. Supports response_variable for use in automations and scripts.
Specify the inventory by ID or name (case-insensitive), but not both:
service: simple_inventory.get_items
data:
inventory_id: "01JYFPCDMBRBRK4MB3C26S2FKH"
response_variable: resultservice: simple_inventory.get_items
data:
inventory_name: "Kitchen Freezer"
response_variable: resultExample response:
{
"items": [
{
"name": "Frozen Pizza",
"quantity": 5.0,
"unit": "boxes",
"category": "Frozen Foods",
"categories": ["Frozen Foods", "Quick Meals"],
"location": "Kitchen Freezer",
"locations": ["Kitchen Freezer"],
"expiry_date": "2026-06-15",
"expiry_alert_days": 7,
"auto_add_enabled": true,
"auto_add_to_list_quantity": 2.0,
"desired_quantity": 5.0,
"todo_list": "todo.grocery_list",
"todo_quantity_placement": "name",
"price": 8.99,
"description": "Family size pepperoni",
"barcode": "012345678901",
"barcodes": ["012345678901"]
}
]
}Note the dual fields: category/categories and location/locations. The singular fields contain only the first value for backward compatibility. Prefer the array fields.
Retrieve items from every inventory at once. Supports response_variable for use in automations and scripts.
service: simple_inventory.get_items_from_all_inventories
response_variable: resultExample response:
{
"inventories": [
{
"inventory_id": "01JYFPCDMBRBRK4MB3C26S2FKH",
"inventory_name": "Kitchen Freezer",
"description": "Frozen items",
"items": [
{
"name": "Frozen Pizza",
"quantity": 5.0,
"unit": "boxes"
}
]
}
]
}Get consumption analytics for a specific item. Requires at least 2 decrement events to produce meaningful data. Supports response_variable for use in automations and scripts.
service: simple_inventory.get_item_consumption_rates
data:
inventory_id: "01JYFPCDMBRBRK4MB3C26S2FKH"
name: "Frozen Pizza"
window_days: 30
response_variable: ratesOnly inventory_id and name are required. Omit window_days to use all history.
Example response:
{
"item_name": "Frozen Pizza",
"current_quantity": 3.0,
"unit": "boxes",
"decrement_count": 12,
"total_consumed": 18.0,
"window_days": 30,
"daily_rate": 0.6,
"weekly_rate": 4.2,
"days_until_depletion": 5,
"avg_restock_days": 14.0,
"has_sufficient_data": true,
"total_spend": 53.94,
"daily_spend_rate": 1.80,
"weekly_spend_rate": 12.58
}| Field | Description |
|---|---|
daily_rate |
Average units consumed per day (null if insufficient data) |
weekly_rate |
Average units consumed per week (null if insufficient data) |
days_until_depletion |
Estimated days until quantity reaches 0 (null if rate is 0 or insufficient data) |
avg_restock_days |
Average days between increment events (null if fewer than 2 restocks) |
has_sufficient_data |
true if there are at least 2 decrement events to calculate rates |
total_spend |
Total money spent purchasing this item (null if no priced restock events) |
daily_spend_rate |
Average daily purchasing cost (null if no priced restock events) |
weekly_spend_rate |
Average weekly purchasing cost (null if no priced restock events) |
Search for an item by barcode across all inventories. Useful for finding which inventory contains a scanned item. Supports response_variable for use in automations and scripts.
service: simple_inventory.lookup_by_barcode
data:
barcode: "012345678901"
response_variable: resultExample response:
{
"items": [
{
"name": "Frozen Pizza",
"quantity": 5.0,
"inventory_id": "01JYFPCDMBRBRK4MB3C26S2FKH",
"inventory_name": "Kitchen Freezer"
}
]
}Returns an empty list if no match is found. If the same barcode exists in multiple inventories, all matches are returned.
Scan a barcode and perform an action on the matched item. Automatically resolves which inventory contains the barcode. Supports response_variable for use in automations and scripts.
service: simple_inventory.scan_barcode
data:
barcode: "012345678901"
action: "increment"
amount: 1
response_variable: result| Field | Required | Description |
|---|---|---|
barcode |
Yes | The barcode to scan |
action |
Yes | "increment", "decrement", or "lookup" |
amount |
No | Amount to increment/decrement (default: 1, ignored for lookup) |
price |
No | Unit price to record for this transaction (updates the item's stored price) |
inventory_id |
No | Scope the search to a specific inventory. Required if the barcode exists in multiple inventories. |
Example response (increment/decrement):
{
"action": "increment",
"success": true,
"item_name": "Frozen Pizza",
"inventory_id": "01JYFPCDMBRBRK4MB3C26S2FKH",
"amount": 1.0
}Example response (lookup):
{
"action": "lookup",
"item": {
"name": "Frozen Pizza",
"quantity": 5.0,
"inventory_id": "01JYFPCDMBRBRK4MB3C26S2FKH",
"inventory_name": "Kitchen Freezer"
},
"inventory_id": "01JYFPCDMBRBRK4MB3C26S2FKH"
}Error cases:
- Barcode not found in any inventory: raises an error
- Barcode found in multiple inventories without
inventory_id: raises an error listing the inventories
The integration provides WebSocket commands for real-time communication. These are used by the companion card and can also be used by custom panels or scripts.
Fetch all items for an inventory.
{
"type": "simple_inventory/list_items",
"inventory_id": "01JYFPCDMBRBRK4MB3C26S2FKH"
}Returns: { "items": [...] }
Fetch a single item by name.
{
"type": "simple_inventory/get_item",
"inventory_id": "01JYFPCDMBRBRK4MB3C26S2FKH",
"name": "Frozen Pizza"
}Returns: { "item": { ... } }
Subscribe to real-time inventory updates. When any item changes, you receive the full updated item list.
{
"type": "simple_inventory/subscribe",
"inventory_id": "01JYFPCDMBRBRK4MB3C26S2FKH"
}Omit inventory_id to subscribe to updates from all inventories:
{
"type": "simple_inventory/subscribe"
}When subscribed to a specific inventory, each event delivers { "items": [...] } with the full item list. When subscribed globally, events deliver { "event": "updated" }.
Query change history for an inventory or a specific item.
{
"type": "simple_inventory/get_history",
"inventory_id": "01JYFPCDMBRBRK4MB3C26S2FKH",
"item_name": "Frozen Pizza",
"event_type": "decrement",
"start_date": "2026-01-01",
"end_date": "2026-02-15",
"limit": 50,
"offset": 0
}All fields except inventory_id are optional. Supported event_type values: add, remove, increment, decrement, update.
Returns: { "events": [...] } where each event has:
event_type— What happenedamount— How much changedquantity_before,quantity_after— Stock levelstimestamp— When it happened
Get consumption analytics for a specific item. Requires at least 2 decrement events to produce meaningful data.
{
"type": "simple_inventory/get_item_consumption_rates",
"inventory_id": "01JYFPCDMBRBRK4MB3C26S2FKH",
"item_name": "Frozen Pizza",
"window_days": 30
}window_days is optional — omit it to calculate rates across all history.
Returns:
{
"item_name": "Frozen Pizza",
"current_quantity": 3.0,
"unit": "boxes",
"decrement_count": 12,
"total_consumed": 18.0,
"window_days": 30,
"daily_rate": 0.6,
"weekly_rate": 4.2,
"days_until_depletion": 5,
"avg_restock_days": 14.0,
"has_sufficient_data": true,
"total_spend": 53.94,
"daily_spend_rate": 1.80,
"weekly_spend_rate": 12.58
}| Field | Description |
|---|---|
daily_rate |
Average units consumed per day (null if insufficient data) |
weekly_rate |
Average units consumed per week (null if insufficient data) |
days_until_depletion |
Estimated days until quantity reaches 0 (null if rate is 0 or insufficient data) |
avg_restock_days |
Average days between increment events (null if fewer than 2 restocks) |
has_sufficient_data |
true if there are at least 2 decrement events to calculate rates |
total_spend |
Total money spent purchasing this item (null if no priced restock events) |
daily_spend_rate |
Average daily purchasing cost (null if no priced restock events) |
weekly_spend_rate |
Average weekly purchasing cost (null if no priced restock events) |
Search for an item by barcode across all inventories.
{
"type": "simple_inventory/lookup_by_barcode",
"barcode": "012345678901"
}Returns: { "items": [...] } — each item includes inventory_id and inventory_name.
Scan a barcode and perform an action on the matched item.
{
"type": "simple_inventory/scan_barcode",
"barcode": "012345678901",
"action": "increment",
"amount": 1.0,
"inventory_id": "01JYFPCDMBRBRK4MB3C26S2FKH"
}Only barcode and action are required. amount defaults to 1. inventory_id is optional unless the barcode exists in multiple inventories.
Returns: { "action": "increment", "success": true, "item_name": "...", "inventory_id": "...", "amount": 1.0 }
Export inventory data.
{
"type": "simple_inventory/export",
"inventory_id": "01JYFPCDMBRBRK4MB3C26S2FKH",
"format": "json"
}Supported formats: json, csv
Import inventory data with a merge strategy.
{
"type": "simple_inventory/import",
"inventory_id": "01JYFPCDMBRBRK4MB3C26S2FKH",
"data": [{"name": "New Item", "quantity": 3}],
"format": "json",
"merge_strategy": "skip"
}Merge strategies:
skip— Skip items that already exist (default)overwrite— Replace existing items with imported datamerge_quantities— Add imported quantities to existing quantities
Returns: { "added": 1, "updated": 0, "skipped": 0, "errors": [] }
automation:
- alias: "Shopping list notification"
trigger:
- platform: event
event_type: simple_inventory_item_added_to_list
action:
- service: notify.mobile_app
data:
title: "Added to shopping list"
message: "{{ trigger.event.data.item_name }} (need {{ trigger.event.data.quantity_needed }})"automation:
- alias: "Out of stock alert"
trigger:
- platform: event
event_type: simple_inventory_item_depleted
action:
- service: notify.mobile_app
data:
title: "Out of stock"
message: "{{ trigger.event.data.item_name }} is depleted!"Use scan_barcode for automatic cross-inventory resolution — no need to know which inventory the item belongs to:
automation:
- alias: "Barcode scan - add to inventory"
trigger:
- platform: event
event_type: tag_scanned
action:
- service: simple_inventory.scan_barcode
data:
barcode: "{{ trigger.event.data.tag_id }}"
action: "increment"
amount: 1automation:
- alias: "Barcode scan - use item"
trigger:
- platform: event
event_type: tag_scanned
condition:
- condition: state
entity_id: input_select.scan_mode
state: "checkout"
action:
- service: simple_inventory.scan_barcode
data:
barcode: "{{ trigger.event.data.tag_id }}"
action: "decrement"
amount: 1
response_variable: result
- service: notify.mobile_app
data:
title: "Checked out"
message: "{{ result.item_name }} (-{{ result.amount }})"script:
find_item_by_barcode:
sequence:
- service: simple_inventory.lookup_by_barcode
data:
barcode: "012345678901"
response_variable: result
- service: notify.mobile_app
data:
title: "Barcode lookup"
message: >
{% if result.items | length == 0 %}
Barcode not found in any inventory.
{% else %}
{% for item in result.items %}
- {{ item.name }} in {{ item.inventory_name }} (qty: {{ item.quantity }})
{% endfor %}
{% endif %}Use an input_select helper to switch between scan modes (increment, decrement, lookup):
automation:
- alias: "Smart barcode scanner"
trigger:
- platform: event
event_type: tag_scanned
action:
- service: simple_inventory.scan_barcode
data:
barcode: "{{ trigger.event.data.tag_id }}"
action: "{{ states('input_select.scan_mode') }}"
amount: "{{ states('input_number.scan_amount') | float(1) }}"
response_variable: result
- service: notify.mobile_app
data:
title: "Scanned: {{ result.item_name }}"
message: >
{% if result.action == 'lookup' %}
Found in inventory {{ result.inventory_id }}
{% else %}
{{ result.action | title }}: {{ result.amount }}
{% endif %}automation:
- alias: "Expiry notification"
trigger:
- platform: state
entity_id: sensor.all_items_expiring_soon
condition:
- condition: numeric_state
entity_id: sensor.all_items_expiring_soon
above: 0
action:
- service: notify.mobile_app
data:
title: "Items expiring soon"
message: >
{{ state_attr('sensor.all_items_expiring_soon', 'total_expiring') }} items expiring soon,
{{ state_attr('sensor.all_items_expiring_soon', 'total_expired') }} already expired.automation:
- alias: "Low stock alert"
trigger:
- platform: state
entity_id: sensor.kitchen_inventory
condition:
- condition: template
value_template: >
{{ state_attr('sensor.kitchen_inventory', 'below_threshold') | length > 0 }}
action:
- service: notify.mobile_app
data:
title: "Low stock items"
message: >
{% for item in state_attr('sensor.kitchen_inventory', 'below_threshold') %}
- {{ item.name }}: {{ item.quantity }} remaining (need {{ item.quantity_needed }})
{% endfor %}Compare recent consumption against the long-term baseline and notify when an item is being used faster than normal:
script:
check_consumption_spike:
sequence:
- service: simple_inventory.get_item_consumption_rates
data:
inventory_id: "01JYFPCDMBRBRK4MB3C26S2FKH"
name: "Milk"
window_days: 30
response_variable: recent
- service: simple_inventory.get_item_consumption_rates
data:
inventory_id: "01JYFPCDMBRBRK4MB3C26S2FKH"
name: "Milk"
response_variable: baseline
- condition: template
value_template: >
{{ recent.daily_rate and baseline.daily_rate and
recent.daily_rate > (baseline.daily_rate * 1.5) }}
- service: notify.mobile_app
data:
title: "Milk consumption spike"
message: >
Recent: {{ recent.daily_rate | round(1) }}/day
vs baseline: {{ baseline.daily_rate | round(1) }}/dayautomation:
- alias: "Depletion warning"
trigger:
- platform: time_pattern
hours: "/6"
action:
- service: simple_inventory.get_item_consumption_rates
data:
inventory_id: "01JYFPCDMBRBRK4MB3C26S2FKH"
name: "Coffee"
response_variable: rates
- condition: template
value_template: >
{{ rates.days_until_depletion is not none and
rates.days_until_depletion <= 7 }}
- service: notify.mobile_app
data:
title: "Coffee running low"
message: >
~{{ rates.days_until_depletion }} days left at current rate
({{ rates.daily_rate | round(1) }} {{ rates.unit }}/day)script:
get_inventory_report:
sequence:
- service: simple_inventory.get_items
data:
inventory_name: "Kitchen Fridge"
response_variable: result
- service: notify.mobile_app
data:
title: "Inventory Report"
message: "You have {{ result.items | length }} items in the fridge."script:
low_stock_summary:
sequence:
- service: simple_inventory.get_items_from_all_inventories
response_variable: all_data
- service: notify.mobile_app
data:
title: "Inventory Summary"
message: >
{% set ns = namespace(low=[]) %}
{% for inv in all_data.inventories %}
{% for item in inv.items if item.auto_add_enabled and item.quantity <= item.auto_add_to_list_quantity %}
{% set ns.low = ns.low + [item.name ~ ' (' ~ inv.inventory_name ~ ')'] %}
{% endfor %}
{% endfor %}
{% if ns.low %}Low stock: {{ ns.low | join(', ') }}{% else %}All stocked up!{% endif %}