Dynamic Weekly Meal Planner for Home Assistant

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

  • :date: Full week meal planning (lunch and dinner)
  • :ice_cube: Automatic freeze detection for common frozen ingredients
  • :iphone: Smart defrost notifications via mobile app
  • :link: Recipe link integration
  • :broom: Bulk clearing options
  • :bar_chart: Dynamic sensors showing today’s and tomorrow’s meals
  • :alarm_clock: Time-based and motion-triggered reminders

Required Components

HACS Integrations

You’ll need these custom components installed via HACS:

  1. Mushroom Cards - Modern, clean card designs
  1. Button Card - Advanced button functionality
  1. 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

  1. Input Text Entities: Store meal plans and recipe links for each day
  2. Template Sensors: Dynamically show today’s and tomorrow’s meals
  3. Binary Sensor: Detects frozen keywords in tomorrow’s dinner
  4. Automations: Send timely defrost reminders
  5. Scripts: Bulk management of meal plans
  6. 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

  1. Add more frozen keywords: Edit the binary sensor template
  2. Change notification times: Modify automation time conditions
  3. Customize mobile devices: Update notification targets in automations
  4. Modify freeze detection delay: Change the 20-hour limit in automation conditions
  5. 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!

1 Like