Hey guys, adding a Shopping List
was on my to do list for a long time. This weekend I took a look at it and added it to our house dashboards so now even kids can add items to the list. I went a bit fancy and added a couple of buttons to quickly add usual items to the list. Basically we have categories
like Dairy
, Deli
, etc. and items like milk
, eggs
, … under each category.
Here’s a few screenshots.
Images
The time consuming part was to find the related photos on google image. I used a few rules for naming the images. A) name should be the same as the item’s name. B) it should be lower case, C) replace spaces to -
, and D) it should be a PNG
file (no background). For example, Dairy
becomes dairy.png
or Meat & Fish
becomes meat-&-fish.png
and I put them in /config/www/images/shopping-list/
which can be referred to as /local/images/shopping-list/
.
Cards
I used custom:button-card
, custom:layout-card
, custom:auto-entities
, browser_mod
, and of course shopping-list
card.
For button cards I made two templates
, one for the categories and one for the items:
shopping_list_card:
entity:
variables:
category: Dairy
name: Milk
quantity: ''
unit: ''
show_icon: false
show_name: true
name: '[[[return variables.name]]]'
icon: mdi:cart
aspect_ratio: 4/3
entity_picture: '[[[return "/local/images/shopping-list/" + variables.name.toLowerCase().replaceAll(" ", "-") + ".png?v5"]]]'
show_entity_picture: true
tap_action:
action: call-service
service: shopping_list.add_item
data:
name: '[[[return variables.category + " - " + variables.name + variables.quantity + variables.unit ]]]'
# ToDo: above string concatenation needs space in between (if quantity is not '')
shopping_list_category_card:
entity:
variables:
name: Fruits & Vegetables
show_icon: false
show_name: true
aspect_ratio: 4/3
name: '[[[return variables.name]]]'
icon: mdi:cart
entity_picture: '[[[return "/local/images/shopping-list/" + variables.name.toLowerCase().replaceAll(" ", "-") + ".png?v5"]]]'
show_entity_picture: true
style:
- padding: 0%
styles:
entity_picture:
- width: 60%
And here’s the dashboard:
title: Shopping
path: shopping
icon: mdi:cart-outline
panel: true
badges: []
cards:
- type: custom:layout-card
layout_type: grid
layout:
grid-template-columns: 50% 50%
grid-template-rows: auto
cards:
- type: shopping-list
title: Shopping List
- type: custom:auto-entities
filter:
template: |
{% set ns = namespace(result = [], categories = {}, items = []) %}
{% set ns.categories =
{
"Dairy": ["Milk","Eggs", "Yoghurt", "Butter", "Ice-cream", "Cheese"],
"Fruits & Vegetables": ["Apples", "Bananas", "Strawberries", "avocados", "Tomatoes", "Cucumbers", "Carrots", "Onions", "Broccoli", "Lettuce"],
"Drink": ["Milk", "Coke", "Beer"],
"Bread & Bakery": ["Toast", "Gluten-free", "Flatbread", "Burger Rolls", "Muffins", "Cookies"],
"Meat & Fish": ["Mince Beef", "Mince Lamb", "Salmon", "Chicken"],
"Deli": ["Cheese", "Salami", "Ham", "Turkey"],
"Snacks": ["Chips", "Pretzels", "Popcorn", "Crackers", "Nuts"],
"Canned Goods": ["Tuna", "Beans", "Diced Tomatoes"],
"Condiments & Spices": ["Sugar", "Olive oil", "Tomato Sauce", "Mayonnaise"],
"Baking": ["Flour", "Sugar"],
"Household & Cleaning": ["Detergent", "Paper Towels", "Tissues", "Bin Bags", "Aluminum Foil", "Zip Bags"],
"Health Care": ["Panadol", "Toothpaste"],
}
%}
{% for cat in ns.categories %}
{% set ns.items = [] %}
{% for item in ns.categories[cat] %}
{% set ns.items = ns.items + [
{
"type": "custom:button-card",
"template": "shopping_list_card",
"variables":
{
"category": cat,
"name": item,
},
}
]%}
{%endfor%}
{% set ns.result = ns.result + [
{
"type": "custom:button-card",
"template": "shopping_list_category_card",
"variables":{
"name": cat,
},
"tap_action":
{
"action": "fire-dom-event",
"browser_mod":
{
"service": "browser_mod.popup",
"data":
{
"title": cat,
"content":
{
"type": "custom:layout-card",
"layout_type": "grid",
"layout":
{
"grid-template-columns": "25% 25% 25% 25%",
"grid-template-rows": "auto",
},
"cards": ns.items
},
},
},
},
}
]%}
{% endfor %}
{{ns.result}}
card_param: cards
card:
type: custom:layout-card
layout_type: grid
layout:
grid-template-columns: 33% 33% 33%
grid-template-rows: auto
Since I didn’t like copy/paste a lot of cards I used above method which I found a good example here. Basically what I did was to make a dictionary
of categories and items (under the name ns.categories
above) which you may want to edit based on your needs. For example you may need to add another category named Pets
or Gluten-Free
- In that case you also need to add photos to the collection following the naming rules mentioned above. This dictionary is looped over by using custom:auto-entities
and using template
as its filter
. The key point was to make another dictionary-style
section (I’m talking about {% set ns.result = ns.result + [
onwards) which would be passed to custom:layout-card
's cards
section at the bottom. Yeah I know looks so confusing…
That’s it.
Automation
Well without automation what’s HA for, right?
Added an automation to send a notification to our phones when we arrive to our local supermarkets. I added three zones for our three supermarkets in our neighbourhood (don’t make them passive
). Also added two minutes latency to the trigger to be sure we are not just passing by.
# ------------------------------------------------------
# notification
# ------------------------------------------------------
- id: shopping_list_someone_enters_a_shop_show_notification
alias: Shopping List - Someone enters a shop - show notification
description: ''
mode: single
trigger:
- platform: numeric_state
entity_id:
- zone.coles
- zone.woolworths
- zone.aldi
above: "0"
for: 120
condition:
condition: template
value_template: "{{states('sensor.shopping_list_items') != ''}}"
action:
- variables:
persons: "{{ state_attr(trigger.entity_id, 'persons') | list }}"
- repeat:
count: "{{ persons | count }}"
sequence:
# get e.g. "person.jack" and convert to "notify.mobile_app_jack_mobile"
- service: "{{ 'notify.mobile_app_' + persons[repeat.index - 1].replace('person.', '') + '_mobile' }}"
data:
title: Shopping List
message: "Click to view the list"
data:
persistent: false
sticky: true
tag: "shopping-list-zone-entered"
color: yellow
icon: mdi:cart-outline
# iOS URL
url: /dashboard-mobile/shopping-list
# Android URL
clickAction: /dashboard-mobile/shopping-list
Note that I named our phones simply jack_mobile
if we have a person named jack
. You may need to edit above.
I created a sensor that reads the shopping list and makes a text sensor that has everything uncompleted
in a long text. I use that A) as a condition above and B) as a text to show on mobile’s notification (for that to work you may need to generate a Long-Lived-Token). If it’s too much, just remove the condition above and also change the message
to
message: "Click to view the list"
Either way, if you click on the notification it will go to shopping list dashboard url (my case is /dashboard-mobile/shopping-list
).
here’s the sensor (source) (in configuration.yaml
):
sensor:
# -------------------------------------------
# Shopping List
# access through HA API
- platform: rest
name: Shopping List Items
headers:
authorization: !secret shopping_list
content-type: 'application/json'
resource: !secret shopping_list_api
value_template: "{{ value_json | selectattr('complete', 'false') | map(attribute='name') | list | join(', ') | truncate(255) }}"
method: GET
scan_interval: 60
authorization
is “Bearer ABCDEFGH
” and ABCDEFGH
is your Long-Lived-Token that you need to generate. shopping_list_api
is something like http://192.168.1.100:8123/api/shopping_list
I hope it could help someone. I edited a few bits and pieces as I was writing up here. I hope I didn’t make mistakes. I will edit and fix the issues if I saw it’s not working for me.