Files
dadaloop82 1637cc1020 feat: HA sensor enrichment, inventory edit guard, recipe ingredient fix, bread shelf-life
- HA sensor: expiring_list now includes full product details (location, brand,
  category, days_remaining, opened_at, vacuum_sealed, default_quantity, etc.)
- HA sensor: new expired_list attribute with full product details per expired item
- HA sensor: new low_stock_list attribute (items with quantity ≤ 1, full details)
- HA sensor: new sensor=product endpoint (?action=ha_sensor&sensor=product)
  with optional filters: &id=, &name=, &location=
- HA cron webhook: expiry alert items now carry full product details
- Inventory edit: confirm dialog when quantity exceeds unit-specific threshold
  (prevents data loss from unit-confusion typos, e.g. 183 conf instead of 0.183)
- Recipe AI: explicit rule against ingredient form substitution
  (fresh tomatoes ≠ passata, fresh milk ≠ UHT ≠ cream, etc.)
- Shelf-life: opened bread rules (piadina 2d, bauletto/pancarrè 4d, pane 3d)
- docs/wiki: HA page updated with new schema, examples, product endpoint

Closes #125
2026-05-29 05:40:25 +00:00

309 lines
11 KiB
Markdown

# Home Assistant Integration
EverShelf integrates natively with [Home Assistant](https://www.home-assistant.io/) to bring your pantry data into your smart-home automations.
**Capabilities:**
- 📡 **REST sensors** — expose pantry counts as HA sensor entities (expiring, expired, shopping list, total items)
- 🔔 **Webhooks** — trigger HA automations on pantry events (expiry alerts, shopping additions, stock updates)
- 📣 **Push notifications** — send alerts to your phone via any HA `notify.*` service
- 🔊 **TTS on smart speakers** — read recipe steps aloud on any HA `media_player` entity
- ⚙️ **In-app config panel** — configure everything from Settings → 🏠 tab (no need to edit `.env` manually)
---
## Quick Setup
1. **Generate a Long-Lived Access Token** in Home Assistant:
- Open HA → your **Profile** (bottom-left avatar) → **Security****Long-Lived Access Tokens****Create Token**
- Copy the generated token — you won't see it again.
2. **Open EverShelf Settings** → tab **🏠 Home Assistant**.
3. Fill in **Home Assistant URL** (e.g. `http://homeassistant.local:8123`) and paste the token.
4. Click **Test connection** — you should see ✅.
5. Enable the features you want (TTS, Webhooks, REST Sensors) and click **Save HA settings**.
---
## REST Sensors
Add EverShelf pantry data as native HA sensor entities that update automatically.
### Endpoints
| URL | Returns | Sensor |
|-----|---------|--------|
| `/api/?action=ha_sensor` | Items expiring soon (≤`HA_EXPIRY_DAYS` days) | `sensor.evershelf_overview` |
| `/api/?action=ha_sensor&sensor=expired` | Expired items count | `sensor.evershelf_expired` |
| `/api/?action=ha_sensor&sensor=shopping` | Shopping list item count | `sensor.evershelf_shopping` |
| `/api/?action=ha_sensor&sensor=total` | Total pantry items | `sensor.evershelf_total` |
| `/api/?action=ha_sensor&sensor=product` | Full inventory — all items with complete details | `sensor.evershelf_products` |
| `/api/?action=ha_sensor&sensor=product&id=42` | Full details for inventory row `id=42` | — |
| `/api/?action=ha_sensor&sensor=product&name=milk` | Full details for items whose name contains "milk" | — |
| `/api/?action=ha_sensor&sensor=product&location=frigo` | All items in a specific location | — |
### Generate & Copy YAML
In Settings → 🏠 Home Assistant → **REST Sensors** card, click **Copy YAML** to get a ready-to-paste `configuration.yaml` block that already contains your EverShelf URL.
### Manual YAML example
```yaml
# configuration.yaml
sensor:
- platform: rest
name: "EverShelf Overview"
unique_id: evershelf_overview
resource: "http://YOUR_EVERSHELF_URL/api/?action=ha_sensor"
scan_interval: 300 # seconds
value_template: "{{ value_json.state }}"
json_attributes:
- expiring_soon
- expiring_3d
- expired_items
- total_items
- shopping_items
- expiring_list # full product details for expiring items
- expired_list # full product details for expired items
- low_stock_list # full product details for items with quantity ≤ 1
- next_expiry_name
- next_expiry_date
- days_to_next_expiry
- last_updated
unit_of_measurement: "items"
- platform: rest
name: "EverShelf Shopping Count"
unique_id: evershelf_shopping
resource: "http://YOUR_EVERSHELF_URL/api/?action=ha_sensor&sensor=shopping"
scan_interval: 180
value_template: "{{ value_json.state }}"
unit_of_measurement: "items"
# Full product inventory — each item includes all details (location, brand, category, …)
- platform: rest
name: "EverShelf Products"
unique_id: evershelf_products
resource: "http://YOUR_EVERSHELF_URL/api/?action=ha_sensor&sensor=product"
scan_interval: 600
value_template: "{{ value_json.state }}"
json_attributes:
- items
- last_updated
unit_of_measurement: "items"
```
Restart Home Assistant after editing `configuration.yaml`.
Every product entry inside `expiring_list`, `expired_list`, `low_stock_list`, and `sensor=product` responses follows the same schema:
```json
{
"product_id": 42,
"inventory_id": 7,
"name": "Latte intero",
"brand": "Parmalat",
"category": "Lattiero-caseari",
"quantity": 2.0,
"unit": "conf",
"default_quantity": 1000.0,
"package_unit": "ml",
"location": "frigo",
"expiry_date": "2025-06-15",
"days_remaining": 3,
"opened_at": "2025-06-10",
"vacuum_sealed": false
}
```
Field details:
| Field | Type | Description |
|-------|------|-------------|
| `product_id` | int | Products table ID |
| `inventory_id` | int | Inventory row ID |
| `name` | string | Product name |
| `brand` | string\|null | Brand (if set) |
| `category` | string\|null | Category (if set) |
| `quantity` | float | Current quantity in inventory |
| `unit` | string | Unit (`conf`, `g`, `ml`, `pz`, …) |
| `default_quantity` | float | Default package size (e.g. 1000 for 1-litre carton) |
| `package_unit` | string\|null | Unit of the default package (`g`, `ml`) |
| `location` | string\|null | Storage location (`frigo`, `freezer`, `dispensa`, …) |
| `expiry_date` | string\|null | ISO date `YYYY-MM-DD` |
| `days_remaining` | int\|null | Days until expiry (negative = already expired) |
| `opened_at` | string\|null | ISO date when the package was opened |
| `vacuum_sealed` | bool | Whether the item is vacuum-sealed |
---
## Webhook Automations
EverShelf fires an HTTP POST to your HA webhook URL when pantry events occur.
### Create the HA Webhook Automation
1. HA → **Settings****Automations & Scenes****Create Automation**
2. Click **Add Trigger** → choose **Webhook**
3. HA generates a **Webhook ID** — copy it
4. Paste the ID into **Settings → 🏠 Home Assistant → Webhook ID**
5. Select which events should trigger the webhook
### Supported Events
| Event key | When it fires |
|-----------|--------------|
| `expiry` | Daily cron — items expiring within `HA_EXPIRY_DAYS` days |
| `shopping_add` | Item added to the shopping list |
| `stock_update` | Inventory quantity changed |
| `barcode_scan` | (reserved for future use) |
### Webhook Payload (POST body)
```json
{
"event": "expiry_alert",
"timestamp": "2025-06-12T08:00:00+00:00",
"data": {
"type": "expiring_soon",
"count": 3,
"days": 3,
"summary": "Milk, Yogurt, Butter",
"items": [
{
"product_id": 42,
"inventory_id": 7,
"name": "Milk",
"brand": "Parmalat",
"category": "Dairy",
"quantity": 2.0,
"unit": "conf",
"default_quantity": 1000.0,
"package_unit": "ml",
"location": "frigo",
"expiry_date": "2025-06-14",
"days_remaining": 2,
"opened_at": "2025-06-10",
"vacuum_sealed": false
}
]
}
}
```
### Example: Expiry Alert → Telegram
```yaml
alias: EverShelf Expiry Alert
trigger:
- platform: webhook
webhook_id: "evershelf_webhook_abc123" # ← your Webhook ID
action:
- service: notify.telegram_bot
data:
message: >
🥫 EverShelf: {{ trigger.json.data.count }} product(s) expiring soon
{% for item in trigger.json.data.items %}
— {{ item.name }}{% if item.brand %} ({{ item.brand }}){% endif %} ·
{{ item.quantity }} {{ item.unit }} · 📍 {{ item.location }} ·
expires {{ item.expiry_date }} ({{ item.days_remaining }} days)
{% endfor %}
```
### Example: Automation on location
You can filter by location in the automation template to only alert for fridge items:
```yaml
condition:
- condition: template
value_template: >
{{ trigger.json.data.items | selectattr('location','eq','frigo') | list | length > 0 }}
```
---
## Push Notifications
If you prefer to receive push alerts without using webhooks, configure a **HA notify service** directly:
1. Find your notify service name in HA: **Developer Tools → Services** → search `notify`
2. Paste it into **Settings → 🏠 → Notify service** (e.g. `notify.mobile_app_my_phone`)
3. Save
EverShelf will call this service from the cron job whenever expiry alerts fire.
---
## TTS on Smart Speakers
Read recipe steps aloud on an Amazon Echo, Google Home, Sonos, or any HA `media_player`.
### Configuration
1. Enter the **Entity ID** of your media player (e.g. `media_player.kitchen_display`)
- Find it in HA: **Developer Tools → States**
2. Click **Apply HA preset to TTS tab** — this auto-fills the TTS tab with the correct HA endpoint and auth headers
3. Save settings
### How it Works
When recipe step TTS is triggered, EverShelf calls:
```
POST /api/services/tts/speak
Authorization: Bearer <HA_TOKEN>
{
"entity_id": "media_player.kitchen_display",
"message": "Add 200 g of flour and mix well."
}
```
The request is proxied through the EverShelf PHP backend (avoids CORS / mixed-content issues).
---
## Environment Variables Reference
All settings are configurable from `.env` or from the in-app Settings panel.
| Variable | Default | Description |
|----------|---------|-------------|
| `HA_ENABLED` | `false` | Master switch for all HA features |
| `HA_URL` | _(empty)_ | Base URL of HA instance, no trailing slash |
| `HA_TOKEN` | _(empty)_ | Long-Lived Access Token |
| `HA_TTS_ENTITY` | _(empty)_ | `media_player` entity for TTS |
| `HA_WEBHOOK_ID` | _(empty)_ | Webhook trigger ID from HA automation |
| `HA_WEBHOOK_EVENTS` | `expiry,shopping_add,stock_update` | Comma-separated list of events |
| `HA_NOTIFY_SERVICE` | _(empty)_ | HA notify service (e.g. `notify.mobile_app_phone`) |
| `HA_EXPIRY_DAYS` | `3` | Days before expiry to trigger the daily alert |
---
## Troubleshooting
**Test shows ❌ "Connection failed"**
- Verify the URL is reachable from the EverShelf server (not just your browser)
- If using HTTPS with a self-signed certificate, the server-side cURL request may fail — use HTTP on the local network instead
- Check that port 8123 (or your custom port) is open on the HA host
**Test shows ❌ "bad_token"**
- The Long-Lived Access Token may have expired or been revoked — generate a new one in HA Profile
**Webhook not firing**
- Confirm HA_ENABLED=true and the Webhook ID is exactly as shown in HA
- Check the EverShelf cron is running (`/api/cron_smart_shopping.php` every 5 minutes)
- For shopping/stock events: verify the event name is in `HA_WEBHOOK_EVENTS`
**TTS not speaking**
- Ensure the media player entity is online in HA (check its state in Developer Tools)
- Try the "Apply HA preset to TTS tab" button and send a test from the TTS tab
- Check HA logs for `tts.speak` errors (some platforms require `tts_options`)
**Sensors show unavailable in HA**
- The EverShelf URL must be reachable from the HA host
- If running EverShelf behind a reverse proxy, ensure `/api/` is accessible
- Use `scan_interval` ≥ 60 to avoid hammering the server