Dashboard views reorganized, mealie/roborock automations, indkorsel snapshots, wavin/sonoff docs, varme/sikkerhed updates
@@ -1 +1 @@
|
||||
2026.4.4
|
||||
2026.5.1
|
||||
@@ -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/
|
||||
|
||||
@@ -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,66 +133,31 @@ 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
|
||||
tap_action:
|
||||
action: call-service
|
||||
service: script.turn_on
|
||||
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)
|
||||
- type: button
|
||||
name: Gå til dock
|
||||
icon: mdi:home-import-outline
|
||||
tap_action:
|
||||
action: call-service
|
||||
service: vacuum.return_to_base
|
||||
@@ -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
|
||||
entity_id: lawn_mower.husqvarna_automower
|
||||
|
||||
- type: button
|
||||
name: Stop
|
||||
styles:
|
||||
card:
|
||||
- background-color: rgba(255, 200, 0, 0.25)
|
||||
- border: 1px solid rgba(255, 200, 0, 0.8)
|
||||
icon: mdi:home-import-outline
|
||||
tap_action:
|
||||
action: call-service
|
||||
service: script.turn_on
|
||||
service: lawn_mower.dock
|
||||
target:
|
||||
entity_id: script.ploeneklipper_manuelt_stop
|
||||
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:
|
||||
|
||||
@@ -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 (fredag–torsdag).
|
||||
|
||||
- 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%
|
||||
|
||||
@@ -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
|
||||
@@ -393,8 +393,8 @@ sections:
|
||||
- type: grid
|
||||
cards:
|
||||
- type: gauge
|
||||
entity: sensor.fjernvarme_ventil_anbefalet
|
||||
name: Anbefalet ventilposition (1–5)
|
||||
entity: sensor.fjernvarme_ventil_3_ugers_gennemsnit
|
||||
name: Anbefalet ventilposition – 3 ugers snit (1–5)
|
||||
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)
|
||||
|
||||
@@ -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%
|
||||
@@ -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. 150–200 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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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` på `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.
|
||||
@@ -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,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 }}"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
gaester:
|
||||
name: "Gæster hjemme"
|
||||
icon: mdi:account-group
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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 – 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 – tryk for at opdatere</div>
|
||||
<div class="modal" id="modal">
|
||||
<span class="close" onclick="closeModal()">✕</span>
|
||||
<span class="nav prev" id="navPrev" onclick="navigate(-1)">‹</span>
|
||||
<img id="mimg" src="" alt=""/>
|
||||
<span class="nav next" id="navNext" onclick="navigate(1)">›</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}")
|
||||
@@ -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: '
|
||||
|
||||
@@ -18,92 +18,83 @@
|
||||
</head>
|
||||
<body>
|
||||
<h1>🛒 Bilka ToGo</h1>
|
||||
<p class="sub">Plan 08/05 – 14/05 · 78 varer</p>
|
||||
<p class="sub">Plan 15/05 – 21/05 · 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>
|
||||
|
||||
@@ -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 @@
|
||||
{"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"}}]}
|
||||
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 614 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 31 KiB |