Dashboard views reorganized, mealie/roborock automations, indkorsel snapshots, wavin/sonoff docs, varme/sikkerhed updates

This commit is contained in:
2026-05-16 07:28:28 +02:00
parent bd134bafef
commit f2ac6064b5
214 changed files with 1812 additions and 675 deletions
+1 -1
View File
@@ -1 +1 @@
2026.4.4
2026.5.1
+1 -2
View File
@@ -38,6 +38,7 @@ logger:
homeassistant.components.discovery: error
homeassistant.components.dlna_dmr: error
async_upnp_client: error
automower_ble: critical
recorder:
purge_keep_days: 7
@@ -110,7 +111,6 @@ cover:
template: !include_dir_merge_list include/templates/
group: !include_dir_merge_named include/groups/
mqtt: !include include/mqtt.yaml
sensor: !include_dir_merge_list include/sensors/
automation: !include_dir_merge_list include/automations/
binary_sensor: !include_dir_merge_list include/binary_sensors/
@@ -120,7 +120,6 @@ input_number: !include_dir_merge_named include/input/number/
input_select: !include_dir_merge_named include/input/select/
input_boolean: !include_dir_merge_named include/input/boolean/
input_text: !include_dir_merge_named include/input/text/
command_line: !include_dir_merge_list include/command_line/
light: !include_dir_merge_list include/lights/
panel_iframe: !include_dir_merge_named include/panels/
script: !include_dir_merge_named include/scripts/
+42 -405
View File
@@ -33,275 +33,19 @@ cards:
name: I morgen
icon: mdi:briefcase-outline
# 👨‍👩‍👧‍👦 Familien tryk for at toggle syg/rask
- type: grid
columns: 4
square: false
cards:
- type: custom:button-card
entity: person.daniel_schusler_dethlefsen
# 👨‍👩‍👧‍👦 Familien
- type: glance
entities:
- entity: person.daniel_schusler_dethlefsen
name: Daniel
show_name: true
show_state: false
show_label: true
show_icon: false
show_entity_picture: true
label: >
[[[
const s = entity.state;
const sick = states['input_select.daniel_status'] &&
states['input_select.daniel_status'].state === 'syg';
const loc = s === 'home' ? 'Hjemme' : s === 'not_home' ? 'Ude' : s;
return sick ? loc + ' · Syg' : loc;
]]]
styles:
card:
- padding: 8px 4px
- border: >
[[[
return states['input_select.daniel_status'] &&
states['input_select.daniel_status'].state === 'syg'
? '2px solid rgba(220,50,50,0.8)' : '2px solid transparent';
]]]
- border-radius: 12px
entity_picture:
- width: 60px
- height: 60px
- border-radius: 50%
- object-fit: cover
- filter: >
[[[
return states['input_select.daniel_status'] &&
states['input_select.daniel_status'].state === 'syg'
? 'grayscale(100%)' : 'none';
]]]
name:
- font-size: 12px
- font-weight: 600
- padding-top: 6px
- color: >
[[[
return states['input_select.daniel_status'] &&
states['input_select.daniel_status'].state === 'syg'
? 'rgb(220,50,50)' : 'var(--primary-text-color)';
]]]
label:
- font-size: 10px
- color: var(--secondary-text-color)
- padding-bottom: 2px
tap_action:
action: call-service
service: input_select.select_option
service_data:
entity_id: input_select.daniel_status
option: >
[[[
return states['input_select.daniel_status'] &&
states['input_select.daniel_status'].state === 'syg'
? 'hjemme' : 'syg';
]]]
hold_action:
action: more-info
entity: person.daniel_schusler_dethlefsen
- type: custom:button-card
entity: person.claus_dethlefsen
- entity: person.claus_dethlefsen
name: Claus
show_name: true
show_state: false
show_label: true
show_icon: false
show_entity_picture: true
label: >
[[[
const s = entity.state;
const sick = states['input_select.claus_status'] &&
states['input_select.claus_status'].state === 'syg';
const loc = s === 'home' ? 'Hjemme' : s === 'not_home' ? 'Ude' : s;
return sick ? loc + ' · Syg' : loc;
]]]
styles:
card:
- padding: 8px 4px
- border: >
[[[
return states['input_select.claus_status'] &&
states['input_select.claus_status'].state === 'syg'
? '2px solid rgba(220,50,50,0.8)' : '2px solid transparent';
]]]
- border-radius: 12px
entity_picture:
- width: 60px
- height: 60px
- border-radius: 50%
- object-fit: cover
- filter: >
[[[
return states['input_select.claus_status'] &&
states['input_select.claus_status'].state === 'syg'
? 'grayscale(100%)' : 'none';
]]]
name:
- font-size: 12px
- font-weight: 600
- padding-top: 6px
- color: >
[[[
return states['input_select.claus_status'] &&
states['input_select.claus_status'].state === 'syg'
? 'rgb(220,50,50)' : 'var(--primary-text-color)';
]]]
label:
- font-size: 10px
- color: var(--secondary-text-color)
- padding-bottom: 2px
tap_action:
action: call-service
service: input_select.select_option
service_data:
entity_id: input_select.claus_status
option: >
[[[
return states['input_select.claus_status'] &&
states['input_select.claus_status'].state === 'syg'
? 'hjemme' : 'syg';
]]]
hold_action:
action: more-info
entity: person.claus_dethlefsen
- type: custom:button-card
entity: person.anne_schusler_dethlefsen
- entity: person.anne_schusler_dethlefsen
name: Anne
show_name: true
show_state: false
show_label: true
show_icon: false
show_entity_picture: true
label: >
[[[
const s = entity.state;
const sick = states['input_select.anne_status'] &&
states['input_select.anne_status'].state === 'syg';
const loc = s === 'home' ? 'Hjemme' : s === 'not_home' ? 'Ude' : s;
return sick ? loc + ' · Syg' : loc;
]]]
styles:
card:
- padding: 8px 4px
- border: >
[[[
return states['input_select.anne_status'] &&
states['input_select.anne_status'].state === 'syg'
? '2px solid rgba(220,50,50,0.8)' : '2px solid transparent';
]]]
- border-radius: 12px
entity_picture:
- width: 60px
- height: 60px
- border-radius: 50%
- object-fit: cover
- filter: >
[[[
return states['input_select.anne_status'] &&
states['input_select.anne_status'].state === 'syg'
? 'grayscale(100%)' : 'none';
]]]
name:
- font-size: 12px
- font-weight: 600
- padding-top: 6px
- color: >
[[[
return states['input_select.anne_status'] &&
states['input_select.anne_status'].state === 'syg'
? 'rgb(220,50,50)' : 'var(--primary-text-color)';
]]]
label:
- font-size: 10px
- color: var(--secondary-text-color)
- padding-bottom: 2px
tap_action:
action: call-service
service: input_select.select_option
service_data:
entity_id: input_select.anne_status
option: >
[[[
return states['input_select.anne_status'] &&
states['input_select.anne_status'].state === 'syg'
? 'hjemme' : 'syg';
]]]
hold_action:
action: more-info
entity: person.anne_schusler_dethlefsen
- type: custom:button-card
entity: person.andreas_schusler_dethlefsen
- entity: person.andreas_schusler_dethlefsen
name: Andreas
show_name: true
show_state: false
show_label: true
show_icon: false
show_entity_picture: true
label: >
[[[
const s = entity.state;
const sick = states['input_select.andreas_status'] &&
states['input_select.andreas_status'].state === 'syg';
const loc = s === 'home' ? 'Hjemme' : s === 'not_home' ? 'Ude' : s;
return sick ? loc + ' · Syg' : loc;
]]]
styles:
card:
- padding: 8px 4px
- border: >
[[[
return states['input_select.andreas_status'] &&
states['input_select.andreas_status'].state === 'syg'
? '2px solid rgba(220,50,50,0.8)' : '2px solid transparent';
]]]
- border-radius: 12px
entity_picture:
- width: 60px
- height: 60px
- border-radius: 50%
- object-fit: cover
- filter: >
[[[
return states['input_select.andreas_status'] &&
states['input_select.andreas_status'].state === 'syg'
? 'grayscale(100%)' : 'none';
]]]
name:
- font-size: 12px
- font-weight: 600
- padding-top: 6px
- color: >
[[[
return states['input_select.andreas_status'] &&
states['input_select.andreas_status'].state === 'syg'
? 'rgb(220,50,50)' : 'var(--primary-text-color)';
]]]
label:
- font-size: 10px
- color: var(--secondary-text-color)
- padding-bottom: 2px
tap_action:
action: call-service
service: input_select.select_option
service_data:
entity_id: input_select.andreas_status
option: >
[[[
return states['input_select.andreas_status'] &&
states['input_select.andreas_status'].state === 'syg'
? 'hjemme' : 'syg';
]]]
hold_action:
action: more-info
entity: person.andreas_schusler_dethlefsen
- entity: binary_sensor.family_presence
name: Familie
# 🪟 Gardiner
- type: grid
@@ -389,71 +133,36 @@ cards:
icon: mdi:floor-plan
tap_action:
action: call-service
service: script.turn_on
service: button.press
target:
entity_id: script.roborock_manuelt_kokken
entity_id: button.roborock_s8_pro_ultra_kokken_bryggers
- type: button
name: Syd
icon: mdi:floor-plan
tap_action:
action: call-service
service: script.turn_on
service: button.press
target:
entity_id: script.roborock_manuelt_syd
entity_id: button.roborock_s8_pro_ultra_syd
- type: button
name: Mop
icon: mdi:floor-plan
tap_action:
action: call-service
service: script.turn_on
service: button.press
target:
entity_id: script.roborock_manuelt_mop
entity_id: button.roborock_s8_pro_ultra_vac_followed_by_mop
- type: custom:button-card
entity: vacuum.roborock_s8_pro_ultra
name: Start
icon: mdi:robot-vacuum
- type: button
name: Gå til dock
icon: mdi:home-import-outline
tap_action:
action: call-service
service: script.turn_on
service: vacuum.return_to_base
target:
entity_id: script.roborock_manuelt_start
state:
- value: cleaning
name: Dock
styles:
card:
- background-color: rgba(255, 200, 0, 0.25)
- border: 1px solid rgba(255, 200, 0, 0.8)
tap_action:
action: call-service
service: vacuum.return_to_base
target:
entity_id: vacuum.roborock_s8_pro_ultra
- value: returning
name: Dock
styles:
card:
- background-color: rgba(255, 200, 0, 0.25)
- border: 1px solid rgba(255, 200, 0, 0.8)
tap_action:
action: call-service
service: vacuum.return_to_base
target:
entity_id: vacuum.roborock_s8_pro_ultra
- value: paused
name: Dock
styles:
card:
- background-color: rgba(255, 200, 0, 0.25)
- border: 1px solid rgba(255, 200, 0, 0.8)
tap_action:
action: call-service
service: vacuum.return_to_base
target:
entity_id: vacuum.roborock_s8_pro_ultra
entity_id: vacuum.roborock_s8_pro_ultra
# 🏎️ Plæneklipper
- type: horizontal-stack
@@ -462,25 +171,8 @@ cards:
entity: input_datetime.ploeneklipper_sidst_koert
show_icon: false
show_name: true
show_state: false
show_label: true
show_state: true
name: Sidst klippet
label: >
[[[
const s = entity.state;
if (!s || s === 'unknown') return 'Ukendt';
const d = new Date(s.replace(' ', 'T'));
if (isNaN(d)) return s;
const now = new Date();
const isToday = d.toDateString() === now.toDateString();
const yesterday = new Date(now);
yesterday.setDate(now.getDate() - 1);
const isYesterday = d.toDateString() === yesterday.toDateString();
const t = d.toLocaleTimeString('da-DK', {hour: '2-digit', minute: '2-digit'});
if (isToday) return 'I dag ' + t;
if (isYesterday) return 'I g\u00e5r ' + t;
return d.toLocaleDateString('da-DK', {day: 'numeric', month: 'short'}) + ' ' + t;
]]]
tap_action:
action: none
styles:
@@ -490,34 +182,30 @@ cards:
- font-size: 11px
- color: var(--secondary-text-color)
- padding-bottom: 4px
label:
state:
- white-space: normal
- word-break: break-word
- line-height: 1.2
- font-size: 13px
- text-align: center
- type: custom:button-card
entity: lawn_mower.husqvarna_automower
- type: button
name: Klip
icon: mdi:robot-mower
tap_action:
action: call-service
service: script.turn_on
service: lawn_mower.start_mowing
target:
entity_id: script.ploeneklipper_manuelt_start
state:
- value: mowing
name: Stop
styles:
card:
- background-color: rgba(255, 200, 0, 0.25)
- border: 1px solid rgba(255, 200, 0, 0.8)
tap_action:
action: call-service
service: script.turn_on
target:
entity_id: script.ploeneklipper_manuelt_stop
entity_id: lawn_mower.husqvarna_automower
- type: button
name: Stop
icon: mdi:home-import-outline
tap_action:
action: call-service
service: lawn_mower.dock
target:
entity_id: lawn_mower.husqvarna_automower
# 💡 Lys kontrol
- type: horizontal-stack
@@ -542,60 +230,17 @@ cards:
action: more-info
show_state: true
- type: custom:button-card
- type: tile
entity: binary_sensor.garageport
name: Garage
show_name: true
show_state: false
show_label: true
label: >
[[[
const isOpen = entity.state === 'on';
const lastChanged = new Date(entity.last_changed);
const secsAgo = (Date.now() - lastChanged) / 1000;
const inMotion = secsAgo < 30;
if (inMotion) return isOpen ? 'Åbner...' : 'Lukker...';
return isOpen ? 'Åben' : 'Lukket';
]]]
icon: >
[[[
return entity.state === 'on'
? 'mdi:garage-open-variant'
: 'mdi:garage-variant';
]]]
extra_styles: |
@keyframes garage-pulse {
0% { opacity: 1; transform: scale(1); }
50% { opacity: 0.55; transform: scale(1.04); }
100% { opacity: 1; transform: scale(1); }
}
styles:
card:
- animation: >
[[[
const secsAgo = (Date.now() - new Date(entity.last_changed)) / 1000;
return secsAgo < 30 ? 'garage-pulse 0.8s ease-in-out infinite' : 'none';
]]]
icon:
- color: >
[[[
const isOpen = entity.state === 'on';
const secsAgo = (Date.now() - new Date(entity.last_changed)) / 1000;
if (secsAgo < 30) return 'dodgerblue';
return isOpen ? 'orange' : 'var(--primary-text-color)';
]]]
label:
- font-size: 11px
- color: >
[[[
const secsAgo = (Date.now() - new Date(entity.last_changed)) / 1000;
return secsAgo < 30 ? 'dodgerblue' : 'var(--secondary-text-color)';
]]]
features_position: bottom
vertical: false
tap_action:
action: call-service
service: cover.toggle
target:
entity_id: cover.anne
show_state: true
# 🎵 Sonos
- type: grid
@@ -741,18 +386,6 @@ cards:
action: call-service
service: script.tv_hygge_announcement
# 🏖️ Ferie
- type: entities
title: Ferie
icon: mdi:beach
entities:
- entity: input_datetime.vacation_start
name: Afrejse
- entity: input_datetime.vacation_end
name: Hjemkomst
- entity: input_boolean.vacation_mode
name: Ferietilstand aktiv
# 🗑️ Affald
- type: glance
columns: 3
@@ -897,6 +530,10 @@ cards:
- entity: input_boolean.guests_mode
name: Vi har gæster
icon: mdi:account-group
- entity: input_boolean.vacation_mode
name: 🌴 Vacation Mode
- entity: input_datetime.vacation_end
name: Slutter
- type: conditional
conditions:
-18
View File
@@ -99,21 +99,3 @@ cards:
| --- | --- |
{{ ns.rows }}
# 🛒 Bilka ToGo - opdater og vis kryds-af liste
- type: vertical-stack
cards:
- type: markdown
content: |
## Bilka ToGo - kryds-af
Tryk på knappen for at hente ingredienser fra ugeplanen (fredagtorsdag).
- type: button
name: Opdater Bilka ToGo-liste nu
icon: mdi:cart-check
tap_action:
action: call-service
service: script.mealie_shopping_refresh
- type: iframe
url: /local/bilka_togo_checklist.html
aspect_ratio: 100%
+215
View File
@@ -0,0 +1,215 @@
title: Vanding
path: vanding
icon: mdi:sprinkler-variant
type: sections
max_columns: 2
sections:
# 💧 Jordfugt målere
- type: grid
cards:
- type: heading
heading: Jordfugt
icon: mdi:water-percent
- type: gauge
entity: sensor.annes_havesensor_soil_moisture_1
name: Højbed 1 Ærter
min: 0
max: 100
needle: true
severity:
green: 40
yellow: 20
red: 0
- type: gauge
entity: sensor.annes_havesensor_soil_moisture_2
name: Højbed 2 Kartofler
min: 0
max: 100
needle: true
severity:
green: 40
yellow: 20
red: 0
- type: gauge
entity: sensor.annes_havesensor_soil_moisture_3
name: Højbed 3 Rabarber
min: 0
max: 100
needle: true
severity:
green: 40
yellow: 20
red: 0
- type: gauge
entity: sensor.annes_havesensor_soil_moisture_4
name: Drivhus
min: 0
max: 100
needle: true
severity:
green: 45
yellow: 25
red: 0
# 📈 Jordfugt historik
- type: grid
cards:
- type: heading
heading: Jordfugt 7 dage
icon: mdi:chart-line
- type: history-graph
title: Højbede (%)
entities:
- entity: sensor.annes_havesensor_soil_moisture_1
name: HB1 Ærter
- entity: sensor.annes_havesensor_soil_moisture_2
name: HB2 Kartofler
- entity: sensor.annes_havesensor_soil_moisture_3
name: HB3 Rabarber
hours_to_show: 168
refresh_interval: 900
- type: history-graph
title: Drivhus (%)
entities:
- entity: sensor.annes_havesensor_soil_moisture_4
name: Drivhus
hours_to_show: 168
refresh_interval: 900
# 🌧️ Regn & vejr
- type: grid
cards:
- type: heading
heading: Regn (Netatmo)
icon: mdi:weather-rainy
- type: tile
entity: sensor.n22_nedbor
name: Nedbør nu
- type: tile
entity: sensor.n22_precipitation_today
name: Nedbør i dag
- type: history-graph
title: Nedbør 7 dage
entities:
- entity: sensor.n22_precipitation_today
name: Nedbør
hours_to_show: 168
refresh_interval: 1800
- type: custom:apexcharts-card
header:
show: true
title: Forventet nedbør næste 7 dage
graph_span: 7d
span:
start: day
apex_config:
chart:
type: bar
height: 200
dataLabels:
enabled: true
formatter: |
EVAL:function(val) { return val ? val + ' mm' : ''; }
xaxis:
type: datetime
labels:
datetimeFormatter:
day: "dd/MM"
yaxis:
min: 0
title:
text: mm
series:
- entity: weather.norgardsvej
name: Nedbør
color: "#4fc3f7"
data_generator: |
return entity.attributes.forecast.map(f => ({
x: new Date(f.datetime).getTime(),
y: f.precipitation ?? 0
}));
# ⏸️ Rain Bird RC2
- type: grid
cards:
- type: heading
heading: Rain Bird RC2
icon: mdi:sprinkler-fire
- type: tile
entity: sensor.annes_vanding_raindelay
name: Regn-forsinkelse status
- type: tile
entity: number.annes_vanding_rain_delay
name: Sæt forsinkelse (dage)
- type: tile
entity: calendar.annes_vanding
name: Vandingsplan
# 🌿 Zonekontrol
- type: grid
cards:
- type: heading
heading: Zoner manuel styring
icon: mdi:water-pump
- type: tile
entity: switch.hojbed_1
name: Højbed 1 Ærter
icon: mdi:sprinkler
- type: tile
entity: switch.hojbed_2
name: Højbed 2 Kartofler
icon: mdi:sprinkler
- type: tile
entity: switch.hojbed_3
name: Højbed 3 Rabarber
icon: mdi:sprinkler
- type: tile
entity: switch.drivhus_drypvanding
name: Drivhus
icon: mdi:greenhouse
# 🔋 Sensorbatterier
- type: grid
cards:
- type: heading
heading: Sensor batterier
icon: mdi:battery
- type: glance
show_name: true
show_icon: true
show_state: true
columns: 4
entities:
- entity: sensor.annes_havesensor_soil_battery_1
name: HB1
icon: mdi:battery
- entity: sensor.annes_havesensor_soil_battery_2
name: HB2
icon: mdi:battery
- entity: sensor.annes_havesensor_soil_battery_3
name: HB3
icon: mdi:battery
- entity: sensor.annes_havesensor_soil_battery_4
name: Drivhus
icon: mdi:battery
+6 -4
View File
@@ -393,8 +393,8 @@ sections:
- type: grid
cards:
- type: gauge
entity: sensor.fjernvarme_ventil_anbefalet
name: Anbefalet ventilposition (15)
entity: sensor.fjernvarme_ventil_3_ugers_gennemsnit
name: Anbefalet ventilposition 3 ugers snit (15)
min: 1
max: 5
needle: true
@@ -412,9 +412,11 @@ sections:
- type: markdown
content: |-
**{{ state_attr('sensor.fjernvarme_ventil_anbefalet', 'anbefaling') }}**
**Anbefalet stilling (3 ugers snit): {{ states('sensor.fjernvarme_ventil_3_ugers_gennemsnit') | float(0) | round(1) }}**
Udetemperatur: {{ state_attr('sensor.fjernvarme_ventil_anbefalet', 'udetemperatur') }}°C
Øjeblikkelig (vejrbaseret): {{ states('sensor.fjernvarme_ventil_anbefalet') }} {{ state_attr('sensor.fjernvarme_ventil_anbefalet', 'anbefaling') }}
Udetemperatur nu: {{ state_attr('sensor.fjernvarme_ventil_anbefalet', 'udetemperatur') }}°C
Gælder for begge manuelle hoveddrejehaner:
- Roth-fordeler (sauna)
+352
View File
@@ -0,0 +1,352 @@
title: Sikkerhed
path: sikkerhed
icon: mdi:shield-home
cards:
# ─────────────────────────────────────────────────────────────
# 📷 KAMERAER
# ─────────────────────────────────────────────────────────────
- type: grid
columns: 2
square: false
cards:
- type: picture-entity
entity: camera.terrasse_sub
name: Terasse
camera_view: live
show_state: false
show_name: true
tap_action:
action: fire-dom-event
browser_mod:
service: browser_mod.popup
data:
title: Terasse Live
content:
type: vertical-stack
cards:
- type: picture-entity
entity: camera.terrasse_sub
camera_view: live
show_name: false
show_state: false
tap_action:
action: none
- type: tile
entity: number.terrasse_focus
name: Fokus
icon: mdi:focus-field
features:
- type: numeric-input
style: slider
- type: tile
entity: number.terrasse_zoom
name: Zoom
icon: mdi:magnify
features:
- type: numeric-input
style: slider
- type: picture-entity
entity: camera.indkoersel_sub
name: Indkørsel
camera_view: live
show_state: false
show_name: true
tap_action:
action: fire-dom-event
browser_mod:
service: browser_mod.popup
data:
title: Indkørsel Live
content:
type: vertical-stack
cards:
- type: picture-entity
entity: camera.indkoersel_sub
camera_view: live
show_name: false
show_state: false
tap_action:
action: none
- type: tile
entity: number.indkoersel_focus
name: Fokus
icon: mdi:focus-field
features:
- type: numeric-input
style: slider
- type: tile
entity: number.indkoersel_zoom
name: Zoom
icon: mdi:magnify
features:
- type: numeric-input
style: slider
# ─────────────────────────────────────────────────────────────
# 🛡️ SIKKERHEDSSTATUS
# ─────────────────────────────────────────────────────────────
- type: heading
heading: Sikkerhedsstatus
heading_style: title
- type: grid
columns: 2
square: false
cards:
# 👥 Tilstedeværelse
- type: custom:mushroom-template-card
entity: binary_sensor.family_presence
primary: >
{{ 'Nogen hjemme' if is_state('binary_sensor.family_presence', 'on') else 'Ingen hjemme' }}
secondary: >
{% set persons = [
('Claus', 'person.claus_dethlefsen'),
('Anne', 'person.anne_schusler_dethlefsen'),
('Andreas', 'person.andreas_schusler_dethlefsen'),
('Daniel', 'person.daniel_schusler_dethlefsen')
] %}
{% set ns = namespace(home=[]) %}
{% for name, eid in persons %}
{% if is_state(eid, 'home') %}{% set ns.home = ns.home + [name] %}{% endif %}
{% endfor %}
{{ ns.home | join(', ') if ns.home else 'Alle ude' }}
icon: >
{{ 'mdi:home-account' if is_state('binary_sensor.family_presence', 'on') else 'mdi:home-outline' }}
icon_color: >
{{ 'green' if is_state('binary_sensor.family_presence', 'on') else 'blue' }}
tap_action:
action: none
# 💡 Lys
- type: custom:mushroom-template-card
entity: light.alle_lys
primary: >
{{ 'Lys er tændt' if is_state('light.alle_lys', 'on') else 'Alt lys slukket' }}
secondary: ""
icon: >
{{ 'mdi:lightbulb-on' if is_state('light.alle_lys', 'on') else 'mdi:lightbulb-off' }}
icon_color: >
{{ 'yellow' if is_state('light.alle_lys', 'on') else 'grey' }}
tap_action:
action: navigate
navigation_path: /lovelace/lys
# 🪟 Vinduer og terrassedør
- type: custom:mushroom-template-card
multiline_secondary: true
primary: >
{% set sensors = [
'binary_sensor.andreas_vindue',
'binary_sensor.daniel_vindue',
'binary_sensor.sovevaerelse_vindue',
'binary_sensor.badevaerelse_vindue',
'binary_sensor.lille_bad_vindue',
'binary_sensor.terrassedor'
] %}
{% set ns = namespace(open=0) %}
{% for s in sensors %}{% if is_state(s, 'on') %}{% set ns.open = ns.open + 1 %}{% endif %}{% endfor %}
{{ 'Alle vinduer lukket' if ns.open == 0 else ns.open | string + ' vindue(r) åben' }}
secondary: >
{% set sensor_map = {
'binary_sensor.andreas_vindue': 'Andreas',
'binary_sensor.daniel_vindue': 'Daniel',
'binary_sensor.sovevaerelse_vindue': 'Soveværelse',
'binary_sensor.badevaerelse_vindue': 'Badeværelse',
'binary_sensor.lille_bad_vindue': 'Lille bad',
'binary_sensor.terrassedor': 'Terrassedør'
} %}
{% set ns = namespace(aabne=[]) %}
{% for s, n in sensor_map.items() %}{% if is_state(s, 'on') %}{% set ns.aabne = ns.aabne + [n] %}{% endif %}{% endfor %}
{{ ns.aabne | join(', ') if ns.aabne else '' }}
icon: >
{% set sensors = [
'binary_sensor.andreas_vindue',
'binary_sensor.daniel_vindue',
'binary_sensor.sovevaerelse_vindue',
'binary_sensor.badevaerelse_vindue',
'binary_sensor.lille_bad_vindue',
'binary_sensor.terrassedor'
] %}
{% set ns = namespace(open=0) %}
{% for s in sensors %}{% if is_state(s, 'on') %}{% set ns.open = ns.open + 1 %}{% endif %}{% endfor %}
{{ 'mdi:window-open-variant' if ns.open > 0 else 'mdi:window-closed-variant' }}
icon_color: >
{% set sensors = [
'binary_sensor.andreas_vindue',
'binary_sensor.daniel_vindue',
'binary_sensor.sovevaerelse_vindue',
'binary_sensor.badevaerelse_vindue',
'binary_sensor.lille_bad_vindue',
'binary_sensor.terrassedor'
] %}
{% set ns = namespace(open=0) %}
{% for s in sensors %}{% if is_state(s, 'on') %}{% set ns.open = ns.open + 1 %}{% endif %}{% endfor %}
{{ 'red' if ns.open > 0 else 'green' }}
tap_action:
action: none
# 🚗 Garage
- type: custom:mushroom-template-card
entity: binary_sensor.garageport
primary: >
{{ 'Garage åben' if is_state('binary_sensor.garageport', 'on') else 'Garage lukket' }}
secondary: >
Sidst ændret: {{ relative_time(states.binary_sensor.garageport.last_changed) }} siden
icon: >
{{ 'mdi:garage-open-variant' if is_state('binary_sensor.garageport', 'on') else 'mdi:garage-variant' }}
icon_color: >
{{ 'orange' if is_state('binary_sensor.garageport', 'on') else 'green' }}
tap_action:
action: call-service
service: cover.toggle
target:
entity_id: cover.anne
# 🏖️ Ferietilstand
- type: custom:mushroom-template-card
entity: input_boolean.vacation_mode
primary: >
{{ 'Ferie aktiv' if is_state('input_boolean.vacation_mode', 'on') else 'Normal tilstand' }}
secondary: >
{% if is_state('input_boolean.vacation_mode', 'on') %}
{% set end = states('input_datetime.vacation_end') %}
{% if end not in ['unknown', 'unavailable', ''] %}Slutter {{ as_datetime(end).strftime('%-d. %b') }}{% endif %}
{% endif %}
icon: >
{{ 'mdi:beach' if is_state('input_boolean.vacation_mode', 'on') else 'mdi:home' }}
icon_color: >
{{ 'cyan' if is_state('input_boolean.vacation_mode', 'on') else 'grey' }}
tap_action:
action: more-info
# 🤖 AI-overvågning (indkørsel)
- type: custom:mushroom-template-card
primary: >
{% set pause = states('input_datetime.ai_indkorsel_ai_pause_until') %}
{% set paused = pause not in ['unknown', 'unavailable', ''] and as_timestamp(pause) > as_timestamp(now()) %}
{{ 'AI-overvågning pauset' if paused else 'AI-overvågning aktiv' }}
secondary: >
{% set pause = states('input_datetime.ai_indkorsel_ai_pause_until') %}
{% set paused = pause not in ['unknown', 'unavailable', ''] and as_timestamp(pause) > as_timestamp(now()) %}
{% if paused %}Genoptages kl. {{ as_datetime(pause).strftime('%H:%M') }}{% else %}Indkørsel overvåges{% endif %}
icon: >
{% set pause = states('input_datetime.ai_indkorsel_ai_pause_until') %}
{% set paused = pause not in ['unknown', 'unavailable', ''] and as_timestamp(pause) > as_timestamp(now()) %}
{{ 'mdi:robot-off' if paused else 'mdi:robot' }}
icon_color: >
{% set pause = states('input_datetime.ai_indkorsel_ai_pause_until') %}
{% set paused = pause not in ['unknown', 'unavailable', ''] and as_timestamp(pause) > as_timestamp(now()) %}
{{ 'orange' if paused else 'green' }}
tap_action:
action: more-info
entity: input_datetime.ai_indkorsel_ai_pause_until
# 🔒 Terrassedør (separat overblik)
- type: custom:mushroom-template-card
entity: binary_sensor.terrassedor
primary: >
{{ 'Terrassedør åben' if is_state('binary_sensor.terrassedor', 'on') else 'Terrassedør lukket' }}
secondary: >
Sidst ændret: {{ relative_time(states.binary_sensor.terrassedor.last_changed) }} siden
icon: >
{{ 'mdi:door-open' if is_state('binary_sensor.terrassedor', 'on') else 'mdi:door-closed' }}
icon_color: >
{{ 'red' if is_state('binary_sensor.terrassedor', 'on') else 'green' }}
tap_action:
action: more-info
# 📡 Bevægelse i indkørslen lige nu
- type: custom:mushroom-template-card
entity: binary_sensor.indkorsel_sensor_motion
primary: >
{{ 'Bevægelse registreret!' if is_state('binary_sensor.indkorsel_sensor_motion', 'on') else 'Ingen bevægelse' }}
secondary: >
Sidst: {{ relative_time(states.binary_sensor.indkorsel_sensor_motion.last_changed) }} siden
icon: >
{{ 'mdi:motion-sensor' if is_state('binary_sensor.indkorsel_sensor_motion', 'on') else 'mdi:motion-sensor-off' }}
icon_color: >
{{ 'red' if is_state('binary_sensor.indkorsel_sensor_motion', 'on') else 'grey' }}
tap_action:
action: none
# ─────────────────────────────────────────────────────────────
# 📋 SENESTE BEVÆGELSE INDKØRSEL (AI-log)
# ─────────────────────────────────────────────────────────────
- type: heading
heading: Seneste bevægelse Indkørsel
heading_style: title
# Seneste snapshot gemt af AI-overvågningsscriptet
- type: picture-entity
entity: camera.indkorsel_snapshot
show_name: false
show_state: false
tap_action:
action: fire-dom-event
browser_mod:
service: browser_mod.popup
data:
title: Seneste bevægelse Indkørsel
content:
type: picture-entity
entity: camera.indkorsel_snapshot
show_name: false
show_state: false
tap_action:
action: none
# Seneste AI-beskrivelse
- type: custom:button-card
entity: input_text.last_notification_message
show_name: false
show_icon: true
show_state: true
icon: mdi:robot
styles:
card:
- padding: 14px 16px
- text-align: left
grid:
- grid-template-areas: '"i s"'
- grid-template-columns: 44px 1fr
- grid-template-rows: auto
icon:
- width: 32px
- height: 32px
- color: var(--primary-color)
- align-self: flex-start
- margin-top: 2px
state:
- white-space: normal
- word-break: break-word
- font-size: 13px
- text-align: left
- line-height: "1.5"
tap_action:
action: none
# ─────────────────────────────────────────────────────────────
# 📸 SENESTE PERSON-SNAPSHOT INDKØRSEL
# ─────────────────────────────────────────────────────────────
- type: heading
heading: Seneste person i indkørsel
heading_style: subtitle
# Klik åbner galleri med alle tidligere snapshots
- type: picture
image: /local/snapshots/indkorsel/latest.jpg
tap_action:
action: navigate
navigation_path: /lovelace/indkorsel-snapshots
# Logbog over bevægelseshændelser (48 timer)
- type: logbook
entities:
- binary_sensor.indkorsel_sensor_motion
hours_to_show: 48
title: Bevægelseslog (48 timer)
@@ -0,0 +1,9 @@
title: Snapshots Indkørsel
path: indkorsel-snapshots
icon: mdi:camera-burst
panel: true
cards:
- type: iframe
url: /local/snapshots/indkorsel_loader.html
aspect_ratio: 100%
+2 -2
View File
@@ -108,11 +108,11 @@
| Antal | Beskrivelse | Status |
|---|---|---|
| 1 stk | Crucial 4GB DDR4-2666 SODIMM — CT4G4SFS8266 | ⬜ Ønsket |
| 1 stk | Crucial 16GB DDR4-2666 SODIMM — CT16G4SFD8266 | ✅ Installeret 13. maj 2026 |
**Baggrund:** OOM-kill (exit 137) 23. april 2026 — Synology løb tør for RAM og dræbte mosquitto, gitea, gitea-db og DokuWiki på samme tid. HA selv overlevede men crashede urent.
**Model:** DS920+ har 4GB loddet + 1 ledig SODIMM-slot. Crucial CT4G4SFS8266 giver 4+4=8GB total. Pris ca. 150200 kr. hos Komplett/Dustin.
**Resultat:** DS920+ kører nu med 4GB (loddet) + 16GB SODIMM = **20GB total RAM**. Bekræftet i DSM Info Center. Sundhedscheck 13. maj 2026: 3,4GB brugt af 19GB tilgængeligt, swap-brug 0B — ingen memory-pressure. Alle 8 Docker containere kører healthy.
---
+252
View File
@@ -0,0 +1,252 @@
# Gulvvarme: Wavin bryggers + køkken → HA styring
## Idiot-sikker installationsguide
**Formål:** Erstatte den dumme RF-modtager (Wavin JT6/3003-boksen) med to Sonoff ZBMINI Zigbee-relæer,
så Home Assistant kan styre bryggers og køkken-gulvvarme præcis som de andre rum.
---
## Del 1: Indkøb
| Vare | Antal | Pris ca. | Link/søg |
|------|-------|----------|----------|
| **Sonoff ZBMINI-L2** (Zigbee relæ, ingen nul-ledning) | 2 | ~130 kr/stk | Aliexpress, Elgiganten |
| **SONOFF SNZB-02D** Zigbee temp/fugt sensor | 2 | ~100 kr/stk | Aliexpress |
> **Vigtigt:** Vælg ZBMINI-**L2** (eller ZBMINI Extreme) den kræver **ikke** en nuleder (N).
> Wavin-boksen har måske ikke nuleder fremme til brug for et relæ.
---
## Del 2: Forståelse af Wavin-boksen
Når du kigger på det grønne printplade med låget af:
```
MAINS IND (fra stikkontakt i væggen):
Brun = FASE (L) "det farlige"
Blå = NUL (N)
KANAL X (til aktuator 1, fx bryggers):
Brun = FASE UD til aktuator
KANAL Y (til aktuator 2, fx køkken):
Brun = FASE UD til aktuator
Aktuatorerne får NUL fra boksen via blå ledning.
```
Boksen virker som et simpelt on/off relæ per kanal:
- Når termostaten sender "varm op" → relæet lukker → 230V fase sendes ud til aktuatoren → ventil åbner
- Sonoff ZBMINI erstatter præcis dette relæ
---
## Del 3: Installation trin for trin
### ⚠️ STOP Sluk strøm FØR du rører noget
1. Find den sikring eller kontakt der forsyner Wavin-boksen
2. Sluk den
3. Brug en spændingsprøver/-tester på de brune ledninger inde i boksen bekræft at der er 0V
---
### Trin 1: Fotografér ledningerne i boksen FØR du piller noget
Tag et billede med din telefon. Du vil gerne huske hvad der sidder hvor.
---
### Trin 2: Identificér de 4 relevante ledninger
I Wavin-boksen sidder:
- **Brun ind** = Fase fra væggen (fælles for begge kanaler)
- **Blå ind** = Nul fra væggen (fælles)
- **Brun ud X** = Fase ud til aktuator bryggers
- **Brun ud Y** = Fase ud til aktuator køkken
(De blå ledninger der går ud er nuleder direkte til aktuatorerne de ændres ikke)
---
### Trin 3: Monter Sonoff ZBMINI-L2 nr. 1 (bryggers)
ZBMINI-L2 har disse klemmer:
```
[ L in ] [ L out ] [ S1 ] [ S2 ]
```
Tilslut:
- **L in** ← Brun fase ind fra væggen (eller tag en aftapning fra eksisterende brun)
- **L out** → Brun fase ud til bryggers-aktuatoren (den ledning der tidligere sad i X-relæet)
- **S1/S2** = bruges kun hvis du vil have en fysisk kontakt lad dem sidde tomme
Sonoff ZBMINI-L2 kræver ikke N (nuleder) det er pointen med L2-modellen.
---
### Trin 4: Monter Sonoff ZBMINI-L2 nr. 2 (køkken)
Identisk som trin 3, men brug Y-kanalens udgang:
- **L in** ← Brun fase ind (kan sidde på samme aftapning som nr. 1)
- **L out** → Brun fase ud til køkken-aktuatoren
---
### Trin 5: Wavin RF-modtagerboksen
Den eksisterende boks kobles nu **forbi** dens relæer bruges ikke længere.
Du kan enten:
- Efterlade den hængende (ufarlig, bare strøm ind og tomme udgange)
- Klippe strømmen til den (tag brun og blå ind ud af klemmerne og tape enderne)
Den gamle Wavin termostat på væggen virker stadig men gør intet du kan efterlade den eller tage den ned.
---
### Trin 6: Gendan strøm og test
1. Sæt strøm til igen
2. Begge Sonoff-enheder bør lyse rødt (venter på pairing)
---
## Del 4: Zigbee-pairing i Home Assistant
1. Gå til **Indstillinger → Enheder → Zigbee2MQTT** (eller ZHA hvis du bruger det)
2. Klik **Tillad tilslutning / Permit join** (60 sekunder)
3. Hold knappen på Sonoff ZBMINI nede i 5 sekunder til LED blinker hurtigt
4. Enheden dukker op navngiv den `bryggers_relæ` og `kokken_relæ`
5. Gentag for temp-sensorerne (tryk lille knap på siden for at parre)
---
## Del 5: Home Assistant konfiguration
### 5a: generic_thermostat (climate entity)
Tilføj til `configuration.yaml` (eller en inkluderet fil):
```yaml
climate:
- platform: generic_thermostat
name: Bryggers
unique_id: generic_thermostat_bryggers
heater: switch.bryggers_relae # Sonoff enhedens switch entity
target_sensor: sensor.bryggers_temp_sensor_temperature
min_temp: 15
max_temp: 28
target_temp: 20
cold_tolerance: 0.3
hot_tolerance: 0.3
min_cycle_duration:
minutes: 5
ac_mode: false
- platform: generic_thermostat
name: Køkken
unique_id: generic_thermostat_kokken
heater: switch.kokken_relae
target_sensor: sensor.kokken_temp_sensor_temperature
min_temp: 15
max_temp: 28
target_temp: 20
cold_tolerance: 0.3
hot_tolerance: 0.3
min_cycle_duration:
minutes: 5
ac_mode: false
```
> Tilpas entity-navnene til hvad Zigbee2MQTT faktisk kalder dem efter pairing.
### 5b: input_number til komforttemperaturer
Tilføj til `include/input/number/varme.yaml`:
```yaml
varme_komfort_bryggers:
name: Komfort - Bryggers
min: 15
max: 28
step: 0.5
unit_of_measurement: "°C"
initial: 20
icon: mdi:thermometer
varme_komfort_kokken:
name: Komfort - Køkken
min: 15
max: 28
step: 0.5
unit_of_measurement: "°C"
initial: 20
icon: mdi:thermometer
```
### 5c: Tilføj til varme_recalculate scriptet
De to nye rum skal med i `include/scripts/varme_styring.yaml``varme_recalculate`
på samme måde som badeværelse og stue (Danfoss Ally-mønsteret):
```yaml
# ---- Bryggers generic_thermostat ----
- if:
- condition: template
value_template: "{{ true }}" # ingen vinduessensor endnu
then:
- service: climate.set_temperature
target:
entity_id: climate.bryggers
data:
hvac_mode: heat
temperature: >
{% set k = states('input_number.varme_komfort_bryggers') | float(20) %}
{% if vacation %} {{ ferie_temp }}
{% elif night %} {{ [k - nat_sænk, 15] | max }}
{% elif not home %} {{ [k - vaek_sænk, 15] | max }}
{% else %} {{ k }}
{% endif %}
# ---- Køkken generic_thermostat ----
- if:
- condition: template
value_template: "{{ true }}"
then:
- service: climate.set_temperature
target:
entity_id: climate.kokken
data:
hvac_mode: heat
temperature: >
{% set k = states('input_number.varme_komfort_kokken') | float(20) %}
{% if vacation %} {{ ferie_temp }}
{% elif night %} {{ [k - nat_sænk, 15] | max }}
{% elif not home %} {{ [k - vaek_sænk, 15] | max }}
{% else %} {{ k }}
{% endif %}
```
---
## Del 6: Verificering
Når alt er sat op:
1. Gå til **Udviklerværktøjer → Tjenester**
2. Kald `climate.set_temperature``climate.bryggers` med `temperature: 25`
3. Lyt efter at aktuatoren klikker (kan høres eller mærkes) inden for 1-2 minutter
4. Sæt tilbage til normal komforttemperatur
---
## Resumé: Hvad du køber
| | |
|---|---|
| 2× Sonoff ZBMINI-L2 | ~260 kr |
| 2× Sonoff SNZB-02D temp-sensor | ~200 kr |
| **Total** | **~460 kr** |
Ingen elektriker, ingen nye kabler til aktuatorerne, ingen cloud-afhængighed.
+24
View File
@@ -1,3 +1,27 @@
- id: badevaerelse_startup_sluk
alias: Badeværelse lys sluk ved HA opstart
description: >
Slukker badeværelsets lys ved genstart hvis bevægelsessensoren er inaktiv.
Sikrer mod lys der sidder tændt efter strømudfald eller HA-genstart.
mode: single
trigger:
- platform: homeassistant
event: start
action:
- delay:
seconds: 30
- condition: state
entity_id: binary_sensor.badevaerelse_bevaegelse
state: "off"
- service: light.turn_off
target:
area_id: badevaerelse
- service: input_boolean.turn_off
target:
entity_id: input_boolean.badevaerelse_manuel_tilstand
- id: badevaerelse_motion_lys
alias: Badeværelse lys via bevægelse
mode: restart
+24 -5
View File
@@ -24,7 +24,8 @@
{% set t = now().strftime('%H%M') | int %}
{% if 600 <= t < 1600 %}morgen
{% elif 1600 <= t < 1900 %}eftermiddag
{% elif 1900 <= t %}aften
{% elif 1900 <= t < 2100 %}aften_lys
{% elif 2100 <= t %}aften
{% else %}nat{% endif %}
timeout_min: >
{% set t = now().strftime('%H%M') | int %}
@@ -32,7 +33,9 @@
{{ states('input_number.stue_timeout_morgen') | int }}
{% elif 1600 <= t < 1900 %}
{{ states('input_number.stue_timeout_eftermiddag') | int }}
{% elif 1900 <= t %}
{% elif 1900 <= t < 2100 %}
{{ states('input_number.stue_timeout_aften') | int }}
{% elif 2100 <= t %}
{{ states('input_number.stue_timeout_aften') | int }}
{% else %}
{{ states('input_number.stue_timeout_nat') | int }}
@@ -50,6 +53,15 @@
{{ states('sensor.stue_belysningsstyrke') | int < lux_limit }}
sequence:
- choose:
# Gæster: altid Annes favorit uanset tidspunkt
- conditions:
- condition: state
entity_id: input_boolean.gaester
state: "on"
sequence:
- service: scene.turn_on
target:
entity_id: scene.stue_annes_favorit
- conditions:
- condition: template
value_template: "{{ dagperiode == 'morgen' }}"
@@ -64,6 +76,13 @@
- service: scene.turn_on
target:
entity_id: scene.stue_annes_favorit
- conditions:
- condition: template
value_template: "{{ dagperiode == 'aften_lys' }}"
sequence:
- service: scene.turn_on
target:
entity_id: scene.stue_annes_favorit
default:
- service: scene.turn_on
target:
@@ -76,7 +95,7 @@
id: motion_off
- condition: template
value_template: >
{{ dagperiode != 'aften' or
{{ dagperiode not in ('aften','aften_lys') or
is_state('media_player.samsung_s95ca_55_3', 'off') }}
sequence:
- delay:
@@ -86,7 +105,7 @@
state: "off"
- condition: template
value_template: >
{{ dagperiode != 'aften' or
{{ dagperiode not in ('aften','aften_lys') or
is_state('media_player.samsung_s95ca_55_3', 'off') }}
- service: light.turn_off
target:
@@ -97,7 +116,7 @@
- condition: trigger
id: tv_off
- condition: template
value_template: "{{ dagperiode == 'aften' }}"
value_template: "{{ dagperiode in ('aften','aften_lys') }}"
sequence:
- delay:
minutes: "{{ timeout_min }}"
+11 -10
View File
@@ -1,13 +1,3 @@
- id: mealie_update_mealplan
alias: "Mealie opdater madplan"
trigger:
- platform: homeassistant
event: start
- platform: time_pattern
minutes: "/30"
action:
- service: shell_command.mealie_update
- id: mealie_generate_bilka_checklist_wednesday
alias: "Mealie indkøbsliste - onsdag morgen"
trigger:
@@ -19,3 +9,14 @@
- wed
action:
- service: script.mealie_shopping_refresh
- id: mealie_update_mealplan
alias: "Mealie opdater madplan"
trigger:
- platform: homeassistant
event: start
- platform: time_pattern
minutes: "/30"
action:
- service: shell_command.mealie_update
+7 -10
View File
@@ -88,13 +88,13 @@
target:
entity_id: button.roborock_s8_pro_ultra_kokken_bryggers
- delay: "00:00:20"
- wait_template: "{{ is_state('vacuum.roborock_s8_pro_ultra', 'cleaning') }}"
timeout: "00:02:00"
- choose:
- conditions:
- condition: state
entity_id: vacuum.roborock_s8_pro_ultra
state: "cleaning"
- condition: template
value_template: "{{ wait.completed }}"
sequence:
- service: input_number.increment
target:
@@ -109,17 +109,14 @@
}} min.
- conditions:
- condition: not
conditions:
- condition: state
entity_id: vacuum.roborock_s8_pro_ultra
state: "cleaning"
- condition: template
value_template: "{{ not wait.completed }}"
sequence:
- service: notify.mobile_app_claus_iphone_15pro
data:
title: "⚠️ Roborock start fejlede"
message: >
Startkommando sendt, men den begyndte ikke at køre.
Startkommando sendt, men den begyndte ikke at køre inden for 2 min.
State: {{ states('vacuum.roborock_s8_pro_ultra') }}.
Status: {{ state_attr('vacuum.roborock_s8_pro_ultra', 'status') | default('ukendt', true) }}.
Error: {{ state_attr('vacuum.roborock_s8_pro_ultra', 'error') | default('ingen', true) }}.
@@ -0,0 +1,26 @@
- alias: 'Snapshot ved person i indkorsel'
description: >
Gemmer et tidsstemplet snapshot + opdaterer latest.jpg + regenererer HTML-galleri,
hver gang binary_sensor.indkoersel_person skifter til 'on'.
trigger:
- platform: state
entity_id: binary_sensor.indkoersel_person
to: 'on'
condition: []
action:
- variables:
ts: "{{ now().strftime('%Y-%m-%d_%H-%M-%S') }}"
# Gem tidsstemplet kopi
- action: camera.snapshot
data:
entity_id: camera.indkoersel_sub
filename: "/config/www/snapshots/indkorsel/{{ ts }}.jpg"
# Overskriv latest.jpg (bruges af local_file-kamera i dashboardet)
- action: camera.snapshot
data:
entity_id: camera.indkoersel_sub
filename: "/config/www/snapshots/indkorsel/latest.jpg"
# Regenerer HTML-galleriet
- action: shell_command.indkorsel_generate_gallery
mode: queued
max: 5
+1 -1
View File
@@ -9,7 +9,7 @@
id: varme_vindue_trigger
description: "Kalder varme_recalculate når et vindue eller terrassedøren skifter tilstand"
mode: queued
max: 3
max: 10
trigger:
- platform: state
entity_id:
+3
View File
@@ -0,0 +1,3 @@
gaester:
name: "Gæster hjemme"
icon: mdi:account-group
+1 -1
View File
@@ -4,7 +4,7 @@ shelly_bagdor_event_cnt:
max: 99999
step: 1
mode: box
initial: -1
initial: 67
shelly_fordor_event_cnt:
name: Shelly fordoer event count
+4 -4
View File
@@ -23,7 +23,7 @@ varme_komfort_sovevaerelse:
max: 28
step: 0.5
unit_of_measurement: "°C"
initial: 18
initial: 20
icon: mdi:thermometer
varme_komfort_kontor:
@@ -32,7 +32,7 @@ varme_komfort_kontor:
max: 28
step: 0.5
unit_of_measurement: "°C"
initial: 19
initial: 20
icon: mdi:thermometer
varme_komfort_gang:
@@ -41,7 +41,7 @@ varme_komfort_gang:
max: 28
step: 0.5
unit_of_measurement: "°C"
initial: 19
initial: 20
icon: mdi:thermometer
varme_komfort_forgang:
@@ -68,7 +68,7 @@ varme_komfort_badevarelse:
max: 28
step: 0.5
unit_of_measurement: "°C"
initial: 20
initial: 24.5
icon: mdi:thermometer
varme_komfort_stue:
+2 -2
View File
@@ -4,5 +4,5 @@ mealie_shopping_refresh:
- service: shell_command.mealie_shopping_merge
- service: notify.mobile_app_claus_iphone_15pro
data:
title: "Bilka ToGo liste opdateret"
message: "Mealie-indkøb (fredag til torsdag) er flettet med Keep-basislisten."
title: "Indkøbsliste opdateret i Mealie"
message: "Indkøbslisten 'Bilka ToGo' er opdateret med opskrifter fra fredag til torsdag."
+4
View File
@@ -15,6 +15,10 @@ overvaagning:
device_id: cf4f218aae515c84aea9f37f190dcfd5
enabled: true
action: camera.snapshot
- action: homeassistant.update_entity
data:
entity_id: camera.indkorsel_snapshot
enabled: true
- delay:
hours: 0
minutes: 0
+13
View File
@@ -0,0 +1,13 @@
##################################################
# Glidende gennemsnit af anbefalet fjernvarme-ventilposition
# Beregner mean over de seneste 21 dage, så anbefalingen
# bevæger sig sæsonmæssigt (uger) i stedet for dagligt.
##################################################
- platform: statistics
name: "Fjernvarme ventil 3 ugers gennemsnit"
entity_id: sensor.fjernvarme_ventil_anbefalet
state_characteristic: mean
sampling_size: 2000
max_age:
days: 21
+1
View File
@@ -0,0 +1 @@
indkorsel_generate_gallery: "python3 /config/python_scripts/generate_indkorsel_gallery.py"
@@ -0,0 +1,155 @@
#!/usr/bin/env python3
"""Generate HTML gallery for indkorsel person-detection snapshots.
Called via shell_command after each new snapshot is saved.
Output: /config/www/snapshots/indkorsel_gallery.html
"""
import os
import glob
from datetime import datetime
SNAPSHOT_DIR = "/config/www/snapshots/indkorsel"
OUTPUT_FILE = "/config/www/snapshots/indkorsel_gallery.html"
LOADER_FILE = "/config/www/snapshots/indkorsel_loader.html"
MAX_SNAPSHOTS = 100
def parse_timestamp(filename):
base = os.path.basename(filename).replace(".jpg", "")
try:
dt = datetime.strptime(base, "%Y-%m-%d_%H-%M-%S")
return dt.strftime("%-d. %b %Y %H:%M:%S")
except Exception:
return base
os.makedirs(SNAPSHOT_DIR, exist_ok=True)
files = sorted(
[f for f in glob.glob(os.path.join(SNAPSHOT_DIR, "*.jpg"))
if os.path.basename(f) != "latest.jpg"],
reverse=True
)[:MAX_SNAPSHOTS]
items_html = ""
images_js = [] # [{src, ts}, ...] for JS navigation
for f in files:
rel = "/local/snapshots/indkorsel/" + os.path.basename(f)
ts = parse_timestamp(f)
idx = len(images_js)
items_html += f"""
<div class="thumb" onclick="openModal({idx})">
<img src="{rel}" loading="lazy" alt="{ts}"/>
<div class="ts">{ts}</div>
</div>"""
images_js.append({"src": rel, "ts": ts})
import json as _json
images_js_str = _json.dumps(images_js)
version = datetime.now().strftime("%Y%m%d%H%M%S")
html = f"""<!DOCTYPE html>
<html lang="da"><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Indkorsel snapshots</title>
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<style>
*{{box-sizing:border-box;margin:0;padding:0}}
body{{background:#111;color:#ddd;font-family:sans-serif;padding:10px}}
h2{{padding:8px 0 14px;font-size:13px;opacity:.5;font-weight:normal}}
.grid{{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:6px}}
.thumb{{cursor:pointer;border-radius:7px;overflow:hidden;background:#222;transition:transform .12s,opacity .12s}}
.thumb:hover{{transform:scale(1.02);opacity:.9}}
.thumb img{{width:100%;aspect-ratio:16/9;object-fit:cover;display:block}}
.ts{{font-size:10px;padding:4px 6px;opacity:.5;text-align:center}}
.modal{{display:none;position:fixed;inset:0;background:rgba(0,0,0,.93);z-index:9999;flex-direction:column;justify-content:center;align-items:center}}
.modal.show{{display:flex}}
.modal img{{max-width:96vw;max-height:82vh;border-radius:8px;object-fit:contain;box-shadow:0 4px 40px rgba(0,0,0,.6)}}
.modal-ts{{margin-top:12px;font-size:14px;opacity:.6;letter-spacing:.3px}}
.modal-counter{{margin-top:4px;font-size:11px;opacity:.35;letter-spacing:.3px}}
.close{{position:absolute;top:14px;right:18px;font-size:32px;cursor:pointer;opacity:.5;line-height:1;user-select:none}}
.close:hover{{opacity:1}}
.nav{{position:absolute;top:50%;transform:translateY(-50%);font-size:44px;cursor:pointer;opacity:.35;user-select:none;padding:0 18px;line-height:1}}
.nav:hover{{opacity:.9}}
.nav.prev{{left:0}} .nav.next{{right:0}}
.nav.disabled{{opacity:.08;cursor:default;pointer-events:none}}
.empty{{padding:40px;text-align:center;opacity:.4;font-size:14px}}
.reload-badge{{position:fixed;bottom:14px;right:14px;background:#1a73e8;color:#fff;padding:6px 14px;border-radius:20px;font-size:12px;cursor:pointer;display:none;z-index:9998}}
</style>
</head><body>
<h2>Viser {len(files)} person-snapshots &ndash; Indkorsel</h2>
{"<div class='grid'>" + items_html + "</div>" if files else "<div class='empty'>Ingen snapshots endnu.</div>"}
<div id="reload-badge" class="reload-badge" onclick="window.location.reload(true)">Nye billeder &#8211; tryk for at opdatere</div>
<div class="modal" id="modal">
<span class="close" onclick="closeModal()">&#x2715;</span>
<span class="nav prev" id="navPrev" onclick="navigate(-1)">&#x2039;</span>
<img id="mimg" src="" alt=""/>
<span class="nav next" id="navNext" onclick="navigate(1)">&#x203a;</span>
<div class="modal-ts" id="mts"></div>
<div class="modal-counter" id="mcounter"></div>
</div>
<script>
const IMGS = {images_js_str};
const VERSION = '{version}';
let cur = 0;
function openModal(idx){{
cur = idx;
render();
document.getElementById('modal').classList.add('show');
}}
function render(){{
const item = IMGS[cur];
document.getElementById('mimg').src = item.src;
document.getElementById('mts').textContent = item.ts;
document.getElementById('mcounter').textContent = (cur+1) + ' / ' + IMGS.length;
document.getElementById('navPrev').classList.toggle('disabled', cur === 0);
document.getElementById('navNext').classList.toggle('disabled', cur === IMGS.length - 1);
}}
function navigate(dir){{
const next = cur + dir;
if(next >= 0 && next < IMGS.length){{ cur = next; render(); }}
}}
function closeModal(){{
document.getElementById('modal').classList.remove('show');
document.getElementById('mimg').src = '';
}}
document.addEventListener('keydown', e => {{
if(!document.getElementById('modal').classList.contains('show')) return;
if(e.key === 'ArrowLeft') navigate(-1);
if(e.key === 'ArrowRight') navigate(1);
if(e.key === 'Escape') closeModal();
}});
document.getElementById('modal').addEventListener('click', function(e){{
if(e.target === this) closeModal();
}});
// Tjek hvert 60 sek om der er en nyere version af galleriet
setInterval(() => {{
fetch('/local/snapshots/indkorsel_loader.html?_=' + Date.now())
.then(r => r.text())
.then(html => {{
const m = html.match(/[?]v=(\\d+)/);
if(m && m[1] !== VERSION) document.getElementById('reload-badge').style.display = 'block';
}}).catch(() => {{}});
}}, 60000);
</script>
</body></html>"""
with open(OUTPUT_FILE, "w", encoding="utf-8") as fh:
fh.write(html)
# Write a tiny loader page that always redirects to gallery with current timestamp
# so the iframe in HA never serves a cached version
cache_bust = version
loader = f"""<!DOCTYPE html>
<html><head>
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<meta http-equiv="refresh" content="0; url=/local/snapshots/indkorsel_gallery.html?v={cache_bust}">
</head><body></body></html>"""
with open(LOADER_FILE, "w", encoding="utf-8") as fh:
fh.write(loader)
print(f"Gallery updated: {len(files)} snapshots -> {OUTPUT_FILE}")
+1 -5
View File
@@ -273,11 +273,7 @@ def main() -> None:
shopping_list_id = reset_shopping_list(MEALIE_BASE_URL, token, TARGET_SHOPPING_LIST_NAME, shopping_list_id)
recipe_ids = get_mealplan_recipe_ids(MEALIE_BASE_URL, token, start_date, end_date)
added_recipes = add_recipes_to_shopping_list(MEALIE_BASE_URL, token, shopping_list_id, recipe_ids)
mealie_items = fetch_mealie_items(MEALIE_BASE_URL, token, shopping_list_id=shopping_list_id)
items = build_items(mealie_items)
write_outputs(root, items, start_date, end_date)
add_recipes_to_shopping_list(MEALIE_BASE_URL, token, shopping_list_id, recipe_ids)
print(
'OK: '
+63 -72
View File
@@ -18,92 +18,83 @@
</head>
<body>
<h1>🛒 Bilka ToGo</h1>
<p class="sub">Plan 08/05 14/05 &nbsp;·&nbsp; 78 varer</p>
<p class="sub">Plan 15/05 21/05 &nbsp;·&nbsp; 69 varer</p>
<table>
<tr><th colspan="2" class="cat">Andet</th></tr>
<tr><td class="cb"><input type="checkbox"></td><td>0,50 tsk chiliflager</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>0,50 tsk røget paprika</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>0,50 dl hvidvin</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>0,50 tsk stødt spidskommen</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 æg</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 æggehvider</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 dl cremefraiche 18 %</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 dl hvidvin</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 dl mælk</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 dl rødvin, eller grøntsagsboullion</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 fed hvidløg, presset</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 iceberg</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 rødløg, i tynde ringe</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 spsk lage fra de syltede cornichoner</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 spsk sennep, - gerne sød</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 spsk sesamfrø</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 tsk garam masala</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 tsk hvide peberkorn (knuste)</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 tsk ketchup</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 tsk majsstivelse</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 knivspids muskatnød, fintrevet</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 knivspids røget paprika</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 knivspids sød paprika</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 spsk smør, til stegning</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 squash, groftrevet</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 tsk sød paprika</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>100 g cheddar</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1,2 kg bagekartofler</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1,2 liter vand</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1/2 tsk chiliflager</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1/2 tsk paprika</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1,50 stødt spidskommen</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1,50 tsk sød paprika</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2 dl grøntsagsbouillon</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2 fed hvidlag (flaekket)</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2 fed hvidlag presset (til marinade)</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2 skalottelag (finthakkede)</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2 spsk smaor</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2 spsk tikka masala paste</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2 spsk toervin (hvidvin)</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2 store lag (finthakkede)</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>20 g cornichoner, meget finthakkede</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>200 g squash, groftrevet</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>3 aeggeblommer</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>3 fed hvidlag (finthakkede)</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>3 fed hvidløg, presset</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>3 spsk smaor</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>300 g gulerødder, i små tern</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>4 fed hvidlag presset (til sauce)</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>4 tortillas pandekager, små</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>400 g spidskål, fintsnittet</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>400g spaghetti eller tagliatelle</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>600g jomfruhummerhaler (optaot, pillede)</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 tsk tørret timian</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>100 g rejer</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>100 g stenbiderrogn</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2 spsk kapers (valgfrit)</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2 spsk smør</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2 tsk tørret oregano</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>200 g lasagneplader</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>3 dl mælk</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>4 grønne asparges</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>4 gulerødder, groftrevet</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>4 hvide asparges</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>4 skiver brød</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>5 stængler bladselleri, groftrevet</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>50 g rasp</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>75 g smør</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>8 rødspættefilet</td></tr>
<tr><th colspan="2" class="cat">Frost</th></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 dl piskefløde</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2 dl piskefloede</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>200g smaor (til bearnaise)</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>50 g mayonnaise</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2 spsk mayonnaise</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2,50 dl piskefløde</td></tr>
<tr><th colspan="2" class="cat">Frugt & Grønt</th></tr>
<tr><td class="cb"><input type="checkbox"></td><td>0,50 agurk, i skiver</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 citron (saft og skal)</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 spsk tomatpure</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2 daaser hakkede tomater (a 400g)</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>200g cherrytomater (halverede)</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>4 tomater, i tern</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 citron saft og fintrevet skal</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 citron, 1 spsk saft herfra</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 citron, skåret i både</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>12 skiver agurk</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2 banan, skåret i skiver på ca. 1 cm</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2 spsk koncentreret tomatpuré</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>4 håndfulde grøn salat, til anretning</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>50 g koncentreret tomatpuré</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>70 g blandet salat</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>800 g hakkede tomater på dåse</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>Grøntsager eller salat</td></tr>
<tr><th colspan="2" class="cat">Kolonial</th></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 1/2 tsk salt (til ris)</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 spsk olivenolie</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 tsk frisk ingefaer revet (til marinade)</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 tsk salt</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1/2 bundt frisk estragon</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2 spsk hvidvinseddike</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2 spsk smaor (til ris)</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2 spsk tandoorikrydderi</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2 tsk frisk ingefaer revet (til sauce)</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>3 spsk olivenolie</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>4 kviste frisk timian</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>600g basmatiris</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>Frisk koriander til servering</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>Salt</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 dl mild chilipasta</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 spsk hvedemel</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 spsk olivenolie, til stegning</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 spsk smør eller olie til stegning</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2 håndfulde frisk dild, til pynt</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2 spsk finvalset havregryn</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2 spsk hvedemel</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2 spsk olivenolie</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>3 dl basmati ris</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>3 spsk olivenolie, til stegning</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>50 g saltede peanuts</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>Ris eller kartofler</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>salt og friskkværnet peber</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>Salt og hvid peber</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>Salt og peber</td></tr>
<tr><th colspan="2" class="cat">Kød & Fisk</th></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 dl persille, finthakket</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 fed hvidløg, finthakket</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 løg, finthakket</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 skalottelag (finthakket)</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1/2 bundt frisk persille (hakket)</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1,2 kg kyllingebryst (i mundrette stykker)</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>4 ribeye steaks a ca. 250g</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2 løg, finthakket</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2 spsk frisk persille, hakket</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2 spsk persille, finthakket</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>4 fed hvidløg, finthakket</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>4 laksefileter med skind (ca. 150 g pr. stk)</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>400 g hakket oksekød</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>50 g cornichoner, finthakket</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>500 g hakket svinekød</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>600 g kyllingebryst, skåret i grove tern</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>75 g bacon, i skiver</td></tr>
<tr><th colspan="2" class="cat">Mejeri & Æg</th></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2 dl yoghurt naturel</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>125 g frisk mozzarella</td></tr>
</table>
</body>
</html>
+96 -132
View File
@@ -1,12 +1,20 @@
{
"count": 78,
"count": 69,
"items": [
{
"name": "0,50 tsk chiliflager",
"name": "0,50 dl hvidvin",
"category": "andet"
},
{
"name": "0,50 tsk røget paprika",
"name": "0,50 tsk stødt spidskommen",
"category": "andet"
},
{
"name": "1 æg",
"category": "andet"
},
{
"name": "1 æggehvider",
"category": "andet"
},
{
@@ -14,7 +22,11 @@
"category": "andet"
},
{
"name": "1 dl hvidvin",
"name": "1 dl mælk",
"category": "andet"
},
{
"name": "1 dl rødvin, eller grøntsagsboullion",
"category": "andet"
},
{
@@ -22,39 +34,23 @@
"category": "andet"
},
{
"name": "1 iceberg",
"name": "1 knivspids muskatnød, fintrevet",
"category": "andet"
},
{
"name": "1 rødløg, i tynde ringe",
"name": "1 knivspids røget paprika",
"category": "andet"
},
{
"name": "1 spsk lage fra de syltede cornichoner",
"name": "1 knivspids sød paprika",
"category": "andet"
},
{
"name": "1 spsk sennep, - gerne sød",
"name": "1 spsk smør, til stegning",
"category": "andet"
},
{
"name": "1 spsk sesamfrø",
"category": "andet"
},
{
"name": "1 tsk garam masala",
"category": "andet"
},
{
"name": "1 tsk hvide peberkorn (knuste)",
"category": "andet"
},
{
"name": "1 tsk ketchup",
"category": "andet"
},
{
"name": "1 tsk majsstivelse",
"name": "1 squash, groftrevet",
"category": "andet"
},
{
@@ -62,223 +58,179 @@
"category": "andet"
},
{
"name": "100 g cheddar",
"name": "1 tsk tørret timian",
"category": "andet"
},
{
"name": "1,2 kg bagekartofler",
"name": "100 g rejer",
"category": "andet"
},
{
"name": "1,2 liter vand",
"name": "100 g stenbiderrogn",
"category": "andet"
},
{
"name": "1/2 tsk chiliflager",
"name": "2 spsk kapers (valgfrit)",
"category": "andet"
},
{
"name": "1/2 tsk paprika",
"name": "2 spsk smør",
"category": "andet"
},
{
"name": "1,50 stødt spidskommen",
"name": "2 tsk tørret oregano",
"category": "andet"
},
{
"name": "1,50 tsk sød paprika",
"name": "200 g lasagneplader",
"category": "andet"
},
{
"name": "2 dl grøntsagsbouillon",
"name": "3 dl mælk",
"category": "andet"
},
{
"name": "2 fed hvidlag (flaekket)",
"name": "4 grønne asparges",
"category": "andet"
},
{
"name": "2 fed hvidlag presset (til marinade)",
"name": "4 gulerødder, groftrevet",
"category": "andet"
},
{
"name": "2 skalottelag (finthakkede)",
"name": "4 hvide asparges",
"category": "andet"
},
{
"name": "2 spsk smaor",
"name": "4 skiver brød",
"category": "andet"
},
{
"name": "2 spsk tikka masala paste",
"name": "5 stængler bladselleri, groftrevet",
"category": "andet"
},
{
"name": "2 spsk toervin (hvidvin)",
"name": "50 g rasp",
"category": "andet"
},
{
"name": "2 store lag (finthakkede)",
"name": "75 g smør",
"category": "andet"
},
{
"name": "20 g cornichoner, meget finthakkede",
"name": "8 rødspættefilet",
"category": "andet"
},
{
"name": "200 g squash, groftrevet",
"category": "andet"
},
{
"name": "3 aeggeblommer",
"category": "andet"
},
{
"name": "3 fed hvidlag (finthakkede)",
"category": "andet"
},
{
"name": "3 fed hvidløg, presset",
"category": "andet"
},
{
"name": "3 spsk smaor",
"category": "andet"
},
{
"name": "300 g gulerødder, i små tern",
"category": "andet"
},
{
"name": "4 fed hvidlag presset (til sauce)",
"category": "andet"
},
{
"name": "4 tortillas pandekager, små",
"category": "andet"
},
{
"name": "400 g spidskål, fintsnittet",
"category": "andet"
},
{
"name": "400g spaghetti eller tagliatelle",
"category": "andet"
},
{
"name": "600g jomfruhummerhaler (optaot, pillede)",
"category": "andet"
},
{
"name": "1 dl piskefløde",
"name": "2 spsk mayonnaise",
"category": "frost"
},
{
"name": "2 dl piskefloede",
"name": "2,50 dl piskefløde",
"category": "frost"
},
{
"name": "200g smaor (til bearnaise)",
"category": "frost"
},
{
"name": "50 g mayonnaise",
"category": "frost"
},
{
"name": "0,50 agurk, i skiver",
"name": "1 citron saft og fintrevet skal",
"category": "frugt & grønt"
},
{
"name": "1 citron (saft og skal)",
"name": "1 citron, 1 spsk saft herfra",
"category": "frugt & grønt"
},
{
"name": "1 spsk tomatpure",
"name": "1 citron, skåret i både",
"category": "frugt & grønt"
},
{
"name": "2 daaser hakkede tomater (a 400g)",
"name": "12 skiver agurk",
"category": "frugt & grønt"
},
{
"name": "200g cherrytomater (halverede)",
"name": "2 banan, skåret i skiver på ca. 1 cm",
"category": "frugt & grønt"
},
{
"name": "4 tomater, i tern",
"name": "2 spsk koncentreret tomatpuré",
"category": "frugt & grønt"
},
{
"name": "1 1/2 tsk salt (til ris)",
"name": "4 håndfulde grøn salat, til anretning",
"category": "frugt & grønt"
},
{
"name": "50 g koncentreret tomatpuré",
"category": "frugt & grønt"
},
{
"name": "70 g blandet salat",
"category": "frugt & grønt"
},
{
"name": "800 g hakkede tomater på dåse",
"category": "frugt & grønt"
},
{
"name": "Grøntsager eller salat",
"category": "frugt & grønt"
},
{
"name": "1 dl mild chilipasta",
"category": "kolonial"
},
{
"name": "1 spsk olivenolie",
"name": "1 spsk hvedemel",
"category": "kolonial"
},
{
"name": "1 tsk frisk ingefaer revet (til marinade)",
"name": "1 spsk olivenolie, til stegning",
"category": "kolonial"
},
{
"name": "1 tsk salt",
"name": "1 spsk smør eller olie til stegning",
"category": "kolonial"
},
{
"name": "1/2 bundt frisk estragon",
"name": "2 håndfulde frisk dild, til pynt",
"category": "kolonial"
},
{
"name": "2 spsk hvidvinseddike",
"name": "2 spsk finvalset havregryn",
"category": "kolonial"
},
{
"name": "2 spsk smaor (til ris)",
"name": "2 spsk hvedemel",
"category": "kolonial"
},
{
"name": "2 spsk tandoorikrydderi",
"name": "2 spsk olivenolie",
"category": "kolonial"
},
{
"name": "2 tsk frisk ingefaer revet (til sauce)",
"name": "3 dl basmati ris",
"category": "kolonial"
},
{
"name": "3 spsk olivenolie",
"name": "3 spsk olivenolie, til stegning",
"category": "kolonial"
},
{
"name": "4 kviste frisk timian",
"name": "50 g saltede peanuts",
"category": "kolonial"
},
{
"name": "600g basmatiris",
"category": "kolonial"
},
{
"name": "Frisk koriander til servering",
"category": "kolonial"
},
{
"name": "Salt",
"name": "Ris eller kartofler",
"category": "kolonial"
},
{
"name": "salt og friskkværnet peber",
"category": "kolonial"
},
{
"name": "Salt og hvid peber",
"category": "kolonial"
},
{
"name": "Salt og peber",
"category": "kolonial"
},
{
"name": "1 dl persille, finthakket",
"name": "1 fed hvidløg, finthakket",
"category": "kød & fisk"
},
{
@@ -286,19 +238,23 @@
"category": "kød & fisk"
},
{
"name": "1 skalottelag (finthakket)",
"name": "2 løg, finthakket",
"category": "kød & fisk"
},
{
"name": "1/2 bundt frisk persille (hakket)",
"name": "2 spsk frisk persille, hakket",
"category": "kød & fisk"
},
{
"name": "1,2 kg kyllingebryst (i mundrette stykker)",
"name": "2 spsk persille, finthakket",
"category": "kød & fisk"
},
{
"name": "4 ribeye steaks a ca. 250g",
"name": "4 fed hvidløg, finthakket",
"category": "kød & fisk"
},
{
"name": "4 laksefileter med skind (ca. 150 g pr. stk)",
"category": "kød & fisk"
},
{
@@ -306,11 +262,19 @@
"category": "kød & fisk"
},
{
"name": "50 g cornichoner, finthakket",
"name": "500 g hakket svinekød",
"category": "kød & fisk"
},
{
"name": "2 dl yoghurt naturel",
"name": "600 g kyllingebryst, skåret i grove tern",
"category": "kød & fisk"
},
{
"name": "75 g bacon, i skiver",
"category": "kød & fisk"
},
{
"name": "125 g frisk mozzarella",
"category": "mejeri & æg"
}
]
+1 -1
View File
@@ -1 +1 @@
{"count": 7, "items": [{"date": "2026-05-15", "recipe": {"name": "Pandestegt laks med citronsm\u00f8r", "slug": "pandestegt-laks-med-citronsmor"}}, {"date": "2026-05-14", "recipe": {"name": "Cheeseburger Tacos", "slug": "cheeseburger-tacos"}}, {"date": "2026-05-13", "recipe": {"name": "K\u00e5lfad med hakket oksek\u00f8d", "slug": "kalfad-med-hakket-oksekod"}}, {"date": "2026-05-12", "recipe": {"name": "Kylling tikka masala med basmatiris", "slug": "kylling-tikka-masala-med-basmatiris-1"}}, {"date": "2026-05-11", "recipe": {"name": "K\u00e5lfad med hakket oksek\u00f8d", "slug": "kalfad-med-hakket-oksekod"}}, {"date": "2026-05-10", "recipe": {"name": "Kylling tikka masala med basmatiris", "slug": "kylling-tikka-masala-med-basmatiris-1"}}, {"date": "2026-05-09", "recipe": {"name": "Pasta med jomfruhummerhaler", "slug": "pasta-med-jomfruhummerhaler-1"}}]}
{"count": 7, "items": [{"date": "2026-05-22", "recipe": {"name": "M\u00f8rbradb\u00f8ffer med bl\u00f8de l\u00f8g og fl\u00f8desauce", "slug": "morbradboffer-med-blode-log-og-flodesauce"}}, {"date": "2026-05-21", "recipe": {"name": "Frikadeller", "slug": "frikadeller"}}, {"date": "2026-05-20", "recipe": {"name": "Rester fra mandag (Lasagne)", "slug": ""}}, {"date": "2026-05-19", "recipe": {"name": "Rester fra s\u00f8ndag (Flyvende Jacob)", "slug": ""}}, {"date": "2026-05-18", "recipe": {"name": "Lasagne", "slug": "lasagne"}}, {"date": "2026-05-17", "recipe": {"name": "Flyvende Jacob", "slug": "flyvende-jacob"}}, {"date": "2026-05-16", "recipe": {"name": "Luksus Stjerneskud", "slug": "luksus-stjerneskud"}}]}
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Some files were not shown because too many files have changed in this diff Show More