Mealie er en selvhostet opskriftssamling med madplan og indkøbsliste. Hos os kører den som App direkte på Home Assistant, og den officielle Mealie-integration binder den sammen med resten af huset. Idéen er enkel: Planlæg ugens mad i Mealie – og så dukker den op i familiens kalender og på mobilen, uden at nogen skal skrive den to steder.
Da jeg skulle skrive dette indlæg, opdagede jeg dog, at den del, der flytter madplanen over i kalenderen, havde fire fejl – som aldrig var blevet opdaget. Mere om det nedenfor.
Hvad integrationen giver
- En kalender pr. måltidstype – morgenmad, frokost, aftensmad, snack, tilbehør, dessert og drikke – med det, der er planlagt i Mealie.
- Indkøbslisten som to-do-liste i Home Assistant, så den kan bruges fra mobilen og med stemmen.
- Handlinger til at hente, oprette, ændre og slette madplaner, søge i opskrifter og importere en opskrift fra en URL.
Sådan hænger det sammen
- Dagens og morgendagens måltider: To automationer kører hver time og ved opstart. De henter dagens og morgendagens begivenheder fra Mealie-kalenderne og gemmer dem som attributter på en variabel (via HACS-integrationen Variable) – én attribut pr. måltidstype.
- Mobil-dashboardet viser to markdown-kort, “Dagens måltider” og “Morgendagens måltider”, som løber variablens attributter igennem og kun viser dem, der har noget planlagt.
- Familiekalenderen: Hver nat kl. 00:05 kopierer en automation de næste syv dages madplan over i kalenderen “Madplan”, som hele familien har på telefonen.

Fire fejl der aldrig viste sig
Synkroniseringen til familiekalenderen så fornuftig ud og kørte hver nat uden fejl. Men den havde aldrig flyttet en eneste ret – og grunden til, at ingen opdagede det, var lige så simpel: Madplanen i Mealie havde været tom i flere måneder. En løkke over en tom liste fejler ikke. Den gør bare ingenting.
Så snart der kom en ret i planen, ville den være gået galt på fire måder:
- Forkerte feltnavne. Automationen læste
meal_typeogdate, men Mealie-integrationen senderentry_typeogmealplan_date. Datoen ville være tom. - Datoen er et dato-objekt.
mealplan_dateer ikke tekst, men et Python-dato-objekt. Det fårrepeat: for_eachtil at afvise hele listen med “must be a list of items”. Løsningen er først at lave listen om til simple tekstværdier. - Ingen dubletkontrol. Hver nat blev de næste syv dage oprettet igen. En ret planlagt en uge frem ville altså ende med at stå i kalenderen op til syv gange. Nu slår automationen først op i kalenderen og springer over, hvis samme dato og titel allerede står der.
- Slutdato = startdato. En heldagsbegivenheds slutdato er eksklusiv – den skal være dagen efter. Med samme dato som start har begivenheden ingen længde.
Sådan testede jeg det
Når planen er tom, kan man ikke se, om noget virker. Integrationen kan heldigvis både oprette og slette måltider, så testen kan ryddes helt op bagefter:
mealie.set_mealplanmed en note som titel – “TEST fra Home Assistant” – til aftensmad i morgen. Svaret viste de rigtige feltnavne.- Kør synkroniseringen: én heldagsbegivenhed i morgen, med den rigtige slutdato.
- Kør den igen: ingen ny begivenhed – dubletkontrollen virker.
- Kør “Hent Mealie i morgen”: testretten står under aftensmad.
mealie.delete_mealplanmed id’et fra trin 1: planen er tom igen.
Automationen
alias: Sync Mealie måltider til kalender
mode: single
variables:
meal_icons:
breakfast: 🌅
lunch: ☀️
dinner: 🌙
snack: 🍎
side: 🥗
dessert: 🍰
drink: 🥤
triggers:
- trigger: time
at: "00:05:00"
actions:
- action: mealie.get_mealplan
data:
config_entry_id: DIN_MEALIE_CONFIG_ENTRY
start_date: "{{ now().strftime('%Y-%m-%d') }}"
end_date: "{{ (now() + timedelta(days=7)).strftime('%Y-%m-%d') }}"
response_variable: plan
# mealplan_date er et dato-objekt – lav listen om til tekst først
- variables:
maaltider: >-
{% set ns = namespace(l=[]) %}
{% for m in plan.mealplan %}
{% set type = (m.entry_type | string | lower).split('.')[-1] %}
{% set ns.l = ns.l + [{
'dato': m.mealplan_date | string,
'type': type,
'titel': meal_icons.get(type, '🍽️') ~ ' ' ~ (m.recipe.name if m.recipe else m.title)}] %}
{% endfor %}
{{ ns.l }}
- action: calendar.get_events
target:
entity_id: calendar.madplan
data:
start_date_time: "{{ now().strftime('%Y-%m-%d') }} 00:00:00"
end_date_time: "{{ (now() + timedelta(days=8)).strftime('%Y-%m-%d') }} 23:59:59"
response_variable: eksisterende
# 'dato|titel' for alt, der allerede står i kalenderen
- variables:
kendte: >-
{% set ns = namespace(l=[]) %}
{% for e in eksisterende['calendar.madplan'].events %}
{% set ns.l = ns.l + [(e.start | string)[:10] ~ '|' ~ e.summary] %}
{% endfor %}
{{ ns.l }}
- repeat:
for_each: "{{ maaltider }}"
sequence:
- if:
- condition: template
value_template: "{{ (repeat.item.dato ~ '|' ~ repeat.item.titel) not in kendte }}"
then:
- action: calendar.create_event
target:
entity_id: calendar.madplan
data:
summary: "{{ repeat.item.titel }}"
start_date: "{{ repeat.item.dato }}"
# heldagsbegivenhed: slutdatoen er eksklusiv
end_date: "{{ (strptime(repeat.item.dato, '%Y-%m-%d') + timedelta(days=1)).strftime('%Y-%m-%d') }}"
description: "Fra Mealie ({{ repeat.item.type }})"Det jeg har lært
- En tom liste skjuler alt. En automation, der løber over en liste, kan være fuld af fejl og stadig køre “grønt” hver nat, så længe listen er tom.
- Kig på det rigtige svar. Feltnavnene står i handlingens svar – kør den én gang i Udviklerværktøjer og se, hvad der faktisk kommer tilbage, før du skriver skabelonerne.
- Alt, der kører igen og igen, skal kunne køre igen. En natlig synkronisering skal tjekke, hvad der allerede findes – ellers bygger den dubletter op.
- Test med data, du kan fjerne igen. Opret en testpost, kør, verificér og slet – så er testen gentagelig og efterlader intet.