Dynamic Weekly Meal Planner for Home Assistant
A comprehensive meal planning system with intelligent freeze detection and defrost reminders. This automation helps you plan your weekly meals and automatically reminds you to defrost frozen ingredients for tomorrow’s dinner.
Features
Full week meal planning (lunch and dinner)
Automatic freeze detection for common frozen ingredients
Smart defrost notifications via mobile app
Recipe link integration
Bulk clearing options
Dynamic sensors showing today’s and tomorrow’s meals
Time-based and motion-triggered reminders
Required Components
HACS Integrations
You’ll need these custom components installed via HACS:
- Mushroom Cards - Modern, clean card designs
- Button Card - Advanced button functionality
- Card Mod - Custom styling for cards
Prerequisites
- Home Assistant with HACS installed
- Mobile app for notifications
- Motion sensor or alarm panel (optional, for motion-triggered reminders)
Installation
Step 1: Configuration.yaml Setup
Add the following to your configuration.yaml
file:
yaml
###############################################################################
# Weekly Meal Planning System
###############################################################################
# 1) Weekly meal planning - lunch & dinner (for UI editing)
input_text:
# --- Lunch ---
lunch_monday: # Monday - Lunch
name: Monday – Lunch
max: 255
lunch_tuesday: # Tuesday - Lunch
name: Tuesday – Lunch
max: 255
lunch_wednesday: # Wednesday - Lunch
name: Wednesday – Lunch
max: 255
lunch_thursday: # Thursday - Lunch
name: Thursday – Lunch
max: 255
lunch_friday: # Friday - Lunch
name: Friday – Lunch
max: 255
lunch_saturday: # Saturday - Lunch
name: Saturday – Lunch
max: 255
lunch_sunday: # Sunday - Lunch
name: Sunday – Lunch
max: 255
# --- Dinner ---
dinner_monday: # Monday - Dinner
name: Monday – Dinner
max: 255
dinner_tuesday: # Tuesday - Dinner
name: Tuesday – Dinner
max: 255
dinner_wednesday: # Wednesday - Dinner
name: Wednesday – Dinner
max: 255
dinner_thursday: # Thursday - Dinner
name: Thursday – Dinner
max: 255
dinner_friday: # Friday - Dinner
name: Friday – Dinner
max: 255
dinner_saturday: # Saturday - Dinner
name: Saturday – Dinner
max: 255
dinner_sunday: # Sunday - Dinner
name: Sunday – Dinner
max: 255
# --- Recipe Links for Dinner ---
dinnerlink_monday: # Recipe Link - Monday Dinner
name: Recipe Link – Monday Dinner
max: 255
dinnerlink_tuesday: # Recipe Link - Tuesday Dinner
name: Recipe Link – Tuesday Dinner
max: 255
dinnerlink_wednesday: # Recipe Link - Wednesday Dinner
name: Recipe Link – Wednesday Dinner
max: 255
dinnerlink_thursday: # Recipe Link - Thursday Dinner
name: Recipe Link – Thursday Dinner
max: 255
dinnerlink_friday: # Recipe Link - Friday Dinner
name: Recipe Link – Friday Dinner
max: 255
dinnerlink_saturday: # Recipe Link - Saturday Dinner
name: Recipe Link – Saturday Dinner
max: 255
dinnerlink_sunday: # Recipe Link - Sunday Dinner
name: Recipe Link – Sunday Dinner
max: 255
# 2) Store timestamp for last defrost reminder
input_datetime:
last_defrost_notice: # Last reminder about defrosting
name: Last defrost reminder
has_date: true
has_time: true
# 3) Template sensors for today's and tomorrow's dinner + freeze detection binary sensor
template:
- sensor:
- name: "Today's Dinner"
unique_id: todays_dinner
state: >
{%- set english_day = now().date().strftime('%A') -%}
{%- set day_mapping = {
'Monday': 'monday',
'Tuesday': 'tuesday',
'Wednesday': 'wednesday',
'Thursday': 'thursday',
'Friday': 'friday',
'Saturday': 'saturday',
'Sunday': 'sunday'
}[english_day] -%}
{{ states('input_text.dinner_' + day_mapping) }}
- sensor:
- name: "Tomorrow's Dinner"
unique_id: tomorrows_dinner
state: >
{%- set english_day = (now().date() + timedelta(days=1)).strftime('%A') -%}
{%- set day_mapping = {
'Monday': 'monday',
'Tuesday': 'tuesday',
'Wednesday': 'wednesday',
'Thursday': 'thursday',
'Friday': 'friday',
'Saturday': 'saturday',
'Sunday': 'sunday'
}[english_day] -%}
{{ states('input_text.dinner_' + day_mapping) }}
# Add more common frozen items below if needed
- binary_sensor:
- name: "Dinner Needs Defrosting"
unique_id: dinner_needs_defrosting
state: >
{% set tomorrow_text = states('sensor.tomorrows_dinner')|lower %}
{{ 'chicken' in tomorrow_text or 'meat' in tomorrow_text or 'pork' in tomorrow_text or 'fish' in tomorrow_text or 'sausage' in tomorrow_text or 'salmon' in tomorrow_text }}
device_class: cold
###############################################################################
Step 2: Automations
Create these three automations in Home Assistant:
Automation 1: Motion-Triggered Defrost Reminder
yaml
alias: Reminder – defrost frozen dinner with motion detector
description: |
Send notification about tomorrow's dinner
containing frozen keywords, between 4:00 PM–8:00 PM when motion sensor triggers.
triggers:
- entity_id: motion_sensor_kitchen # Replace with your motion sensor
to: disarmed
trigger: state
conditions:
- condition: time
after: "16:00:00"
before: "20:00:00"
- condition: state
entity_id: binary_sensor.dinner_needs_defrosting
state: "on"
- condition: template
value_template: >
{% set last = states('input_datetime.last_defrost_notice') %}
{% if last in ['unknown','unavailable',''] %}
true
{% else %}
{{ (as_timestamp(now()) - as_timestamp(last)) > 20*3600 }}
{% endif %}
actions:
- data:
title: 🥶 Time to defrost for tomorrow!
message: "Tomorrow: {{ states('sensor.tomorrows_dinner') }}"
data:
actions:
- action: DEFROSTED
title: I have taken out the food
action: notify.mobile_app_your_phone # Replace with your device
- action: notify.mobile_app_partner_phone # Replace with partner's device
data:
message: "Tomorrow: {{ states('sensor.tomorrows_dinner') }}"
title: 🥶 Time to defrost for tomorrow!
data:
actions:
- action: DEFROSTED
title: I have taken out the food
Automation 2: Scheduled Defrost Reminder
yaml
alias: Reminder – defrost frozen dinner 6:30 PM
description: |
Send notification about tomorrow's dinner
containing frozen keywords, at 6:30 PM if needed.
triggers:
- at: "18:30:00"
trigger: time
conditions:
- condition: state
entity_id: binary_sensor.dinner_needs_defrosting
state: "on"
- condition: template
value_template: >
{% set last = states('input_datetime.last_defrost_notice') %}
{% if last in ['unknown','unavailable',''] %}
true
{% else %}
{{ (as_timestamp(now()) - as_timestamp(last)) > 20*3600 }}
{% endif %}
actions:
- data:
title: 🥶 Time to defrost for tomorrow!
message: "Tomorrow: {{ states('sensor.tomorrows_dinner') }}"
data:
actions:
- action: DEFROSTED
title: I have taken out the food
action: notify.mobile_app_your_phone # Replace with your device
- data:
title: 🥶 Time to defrost for tomorrow!
message: "Tomorrow: {{ states('sensor.tomorrows_dinner') }}"
data:
actions:
- action: DEFROSTED
title: I have taken out the food
action: notify.mobile_app_partner_phone # Replace with partner's device
Automation 3: Handle Defrost Button Response
yaml
alias: Defrost button – handle button press
description: >-
Sets last-reminder-time when you press 'I have taken out the food from
freezer'
triggers:
- event_type: mobile_app_notification_action
event_data:
action: DEFROSTED
trigger: event
actions:
- target:
entity_id: input_datetime.last_defrost_notice
data:
datetime: "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}"
action: input_datetime.set_datetime
mode: single
Step 3: Scripts
Create these two scripts for easy meal plan management:
Script 1: Clear Entire Week
yaml
alias: Clear Entire Week
description: Clears all meals for the entire week
sequence:
- data:
day: monday
action: script.clear_weekday
- data:
day: tuesday
action: script.clear_weekday
- data:
day: wednesday
action: script.clear_weekday
- data:
day: thursday
action: script.clear_weekday
- data:
day: friday
action: script.clear_weekday
- data:
day: saturday
action: script.clear_weekday
- data:
day: sunday
action: script.clear_weekday
mode: single
Script 2: Clear Single Day
yaml
alias: Clear Weekday
description: Clears lunch, dinner and recipe link for a specific day
fields:
day:
description: >-
Which day to clear (monday, tuesday, wednesday, thursday, friday,
saturday, sunday)
example: monday
required: true
selector:
select:
options:
- monday
- tuesday
- wednesday
- thursday
- friday
- saturday
- sunday
sequence:
- target:
entity_id: input_text.lunch_{{ day }}
data:
value: ""
action: input_text.set_value
- target:
entity_id: input_text.dinner_{{ day }}
data:
value: ""
action: input_text.set_value
- target:
entity_id: input_text.dinnerlink_{{ day }}
data:
value: ""
action: input_text.set_value
mode: single
Step 4: Lovelace Dashboard Cards
Main Meal Overview Card
Create a new dashboard view and add this card configuration:
yaml
type: vertical-stack
cards:
- type: custom:mushroom-chips-card
chips:
- type: back
- type: vertical-stack
cards:
- type: custom:mushroom-title-card
title: Weekly Meals
subtitle: ""
- type: custom:mushroom-template-card # Today's dinner display
primary: Today's Dinner
secondary: "{{ states('sensor.todays_dinner') }}"
icon: mdi:silverware-fork-knife
icon_color: teal
tap_action:
action: more-info
entity: sensor.todays_dinner
- type: custom:mushroom-template-card # Tomorrow's dinner display
primary: Tomorrow's Dinner
secondary: "{{ states('sensor.tomorrows_dinner') }}"
icon: mdi:calendar-clock
icon_color: orange
tap_action:
action: more-info
entity: sensor.tomorrows_dinner
- type: custom:mushroom-template-card # Freeze status indicator
primary: Does something need defrosting for tomorrow?
secondary: |-
{% if is_state('binary_sensor.dinner_needs_defrosting', 'on') %}
Yes, time to take it out of the freezer now!
{% else %}
No, nothing to defrost
{% endif %}
icon: |-
{% if is_state('binary_sensor.dinner_needs_defrosting', 'on') %}
mdi:snowflake-alert
{% else %}
mdi:check-circle
{% endif %}
icon_color: |-
{% if is_state('binary_sensor.dinner_needs_defrosting', 'on') %}
blue
{% else %}
green
{% endif %}
tap_action:
action: more-info
entity: binary_sensor.dinner_needs_defrosting
- type: custom:mushroom-template-card # Navigation to planning page
primary: Click to plan weekly meals
secondary: ""
icon: mdi:calendar-week
tap_action:
action: navigate
navigation_path: weekplanning # Create this view path
icon_color: "#800080"
- type: custom:button-card # Advanced defrost control button
entity: input_datetime.last_defrost_notice
variables:
is_snoozed: |
[[[
if (!entity || !entity.state || entity.state === 'unknown' || entity.state === 'unavailable') return false;
const diff = (Date.now() - new Date(entity.state).getTime()) / 1000 / 3600;
return diff < 20;
]]]
name: |
[[[
return variables.is_snoozed
? 'Reminder is silenced, frozen items are now out'
: 'Taken out frozen items? Click here!';
]]]
label: |
[[[
if (variables.is_snoozed) {
return 'Click to reactivate. Last: ' + new Date(entity.state).toLocaleString('en-US');
}
if (entity.state === 'unknown' || entity.state === 'unavailable') {
return 'No reminder dismissed yet';
}
return 'Last dismissed: ' + new Date(entity.state).toLocaleString('en-US');
]]]
icon: mdi:fridge-alert
show_state: false
show_label: true
tap_action:
action: call-service
service: input_datetime.set_datetime
service_data:
entity_id: input_datetime.last_defrost_notice
datetime: |
[[[
if (variables.is_snoozed) {
return '1970-01-01 00:00:00';
} else {
const now = new Date();
const year = now.getFullYear();
const month = (now.getMonth() + 1).toString().padStart(2, '0');
const day = now.getDate().toString().padStart(2, '0');
const hours = now.getHours().toString().padStart(2, '0');
const minutes = now.getMinutes().toString().padStart(2, '0');
const seconds = now.getSeconds().toString().padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
]]]
confirmation:
text: |
[[[
return variables.is_snoozed
? "✅ This will reactivate freeze reminders immediately. Do you want to continue?"
: "⚠️ This will silence all freeze reminders for 20 hours.\n\nAre you sure?";
]]]
hold_action:
action: more-info
styles:
card:
- border-radius: 20px
- padding: 12px
- font-size: 14px
- text-transform: none
- background-color: |
[[[
return variables.is_snoozed
? 'rgba(0, 120, 255, 0.9)'
: 'var(--ha-card-background, var(--card-background-color))';
]]]
- color: |
[[[
return variables.is_snoozed
? '#ffffff'
: 'var(--primary-text-color)';
]]]
name:
- justify-self: start
- text-align: left
- font-weight: bold
label:
- justify-self: start
- text-align: left
- font-size: 12px
- color: |
[[[
return variables.is_snoozed
? '#cfe8ff'
: '#999';
]]]
icon:
- width: 36px
- height: 36px
- justify-self: start
- color: |
[[[
return variables.is_snoozed
? '#ffffff'
: 'var(--primary-color)';
]]]
grid:
- grid-template-areas: |
"i n"
"i l"
- grid-template-columns: auto 1fr
- grid-template-rows: auto auto
extra_styles: |
@keyframes pulse {
0% { box-shadow: 0 0 0px rgba(0,120,255,0.6); }
50% { box-shadow: 0 0 15px rgba(0,120,255,0.9); }
100% { box-shadow: 0 0 0px rgba(0,120,255,0.6); }
}
state:
- value: unknown
styles:
icon:
- color: var(--disabled-text-color)
- operator: template
value: |
[[[
return variables.is_snoozed;
]]]
styles:
card:
- animation: pulse 1.8s infinite
# Weekly meal display sections for each day
- type: vertical-stack
cards:
- type: custom:mushroom-title-card
title: Monday
subtitle: ""
- type: markdown
content: >
## 🍎 Lunch
{{ states('input_text.lunch_monday') if
states('input_text.lunch_monday') not in ['unknown', ''] else '*No
lunch planned*' }}
## 🍽️ Dinner
{{ states('input_text.dinner_monday') if
states('input_text.dinner_monday') not in ['unknown', ''] else '*No
dinner planned*' }}
{% if states('input_text.dinnerlink_monday') and
states('input_text.dinnerlink_monday') != 'unknown' and
states('input_text.dinnerlink_monday') != '' %}
## 🔗 Recipe
[🍽️ Open recipe for Monday dinner]({{
states('input_text.dinnerlink_monday') }})
{% endif %}
card_mod:
style: |
ha-card {
font-size: 1.3rem;
line-height: 1.8;
padding: 20px;
background: var(--card-background-color);
border-radius: 12px;
}
h2 {
margin-top: 16px;
margin-bottom: 8px;
color: var(--primary-text-color);
}
a {
color: var(--accent-color);
text-decoration: none;
font-weight: bold;
}
a:hover {
text-decoration: underline;
}
# Repeat similar sections for Tuesday through Sunday...
# (truncated for brevity - follow same pattern)
Meal Planning Interface Card
Create a second dashboard view called “weekplanning” with this configuration:
yaml
type: vertical-stack
cards:
- type: custom:mushroom-chips-card
chips:
- type: back
- type: horizontal-stack
cards:
- type: custom:mushroom-title-card
title: 📅 Week Planning
subtitle: Plan weekly meals
- type: custom:mushroom-entity-card # Clear entire week button
entity: script.clear_entire_week
name: Clear entire week
icon: mdi:delete-sweep
icon_color: red
tap_action:
action: toggle
confirmation:
text: >-
Are you sure you want to clear all meals for the entire week?
This cannot be undone.
secondary_info: none
layout: vertical
# Day-by-day planning sections
- type: vertical-stack
cards:
- type: custom:mushroom-title-card
title: Monday
subtitle: ""
- type: entities # Input fields for Monday
show_header_toggle: false
entities:
- entity: input_text.lunch_monday
name: Lunch
icon: mdi:food-apple
- entity: input_text.dinner_monday
name: Dinner
icon: mdi:food
- entity: input_text.dinnerlink_monday
name: Recipe for Monday dinner
icon: mdi:link
- type: markdown # Recipe link display
content: >
{% if states('input_text.dinnerlink_monday') and
states('input_text.dinnerlink_monday') != 'unknown' and
states('input_text.dinnerlink_monday') != '' %}
<div class="recipe-button">
<a href="{{ states('input_text.dinnerlink_monday') }}" target="_blank">
🔗 Open recipe 🍽️
</a>
</div>
{% endif %}
card_mod:
style: |
ha-card {
font-size: 1.5rem;
text-align: center;
}
.recipe-button {
text-align: center;
}
.recipe-button a {
font-size: 1.5rem;
font-weight: bold;
}
- type: custom:mushroom-entity-card # Clear Monday button
entity: script.clear_weekday
name: Clear Monday
icon: mdi:broom
icon_color: orange
tap_action:
action: call-service
service: script.clear_weekday
service_data:
day: monday
secondary_info: last-changed
# Repeat for other days of the week...
# (truncated for brevity - follow same pattern)
How It Works
Core Components
- Input Text Entities: Store meal plans and recipe links for each day
- Template Sensors: Dynamically show today’s and tomorrow’s meals
- Binary Sensor: Detects frozen keywords in tomorrow’s dinner
- Automations: Send timely defrost reminders
- Scripts: Bulk management of meal plans
- Dashboard Cards: User-friendly interface for planning and viewing
Freeze Detection Logic
The system automatically detects these frozen keywords in meal names:
- chicken
- meat
- pork
- fish
- sausage
- salmon
You can customize the freeze detection by modifying the binary sensor template in configuration.yaml
.
Notification System
- Motion-triggered: Reminds you between 4-8 PM when motion is detected
- Scheduled: Daily reminder at 6:30 PM
- Smart timing: Won’t spam you - waits 20 hours between reminders
- Interactive: Tap notification button to acknowledge you’ve defrosted
Customization Options
- Add more frozen keywords: Edit the binary sensor template
- Change notification times: Modify automation time conditions
- Customize mobile devices: Update notification targets in automations
- Modify freeze detection delay: Change the 20-hour limit in automation conditions
- Add more meal types: Extend input_text entities for breakfast, snacks, etc.
Troubleshooting
Common Issues
Notifications not working:
- Verify mobile app device names in automations
- Check that Home Assistant companion app is properly configured
- Ensure automation conditions are met (time, freeze detection, etc.)
Freeze detection not working:
- Check that meal names contain the exact keywords (case-insensitive)
- Verify the binary sensor state in Developer Tools
- Add more keywords to the template if needed
Cards not displaying correctly:
- Ensure all required HACS components are installed
- Check for YAML syntax errors in card configuration
- Verify entity names match your configuration
Motion trigger not working:
- Replace the alarm panel entity with your actual motion sensor
- Ensure the entity state changes correctly trigger the automation
Maintenance
- Regularly review and update frozen keyword list
- Adjust notification timing based on your schedule
- Consider seasonal meal planning adjustments
- Update mobile device entity names if phones change
Advanced Features
Adding Breakfast Support
To extend the system for breakfast planning, add these input_text entities:
yaml
input_text:
breakfast_monday:
name: Monday – Breakfast
max: 255
# ... repeat for other days
Integration with Shopping Lists
You can extend this system to automatically add ingredients to shopping lists based on planned meals using additional automations.
Nutritional Tracking
Consider integrating with fitness tracking apps or creating additional sensors to track nutritional information for planned meals.
Conclusion
This dynamic meal planner provides a comprehensive solution for weekly meal management with intelligent freeze detection. The system is designed to be maintenance-free once configured, helping you stay organized and never forget to defrost dinner ingredients again!