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.discovery: error
|
||||||
homeassistant.components.dlna_dmr: error
|
homeassistant.components.dlna_dmr: error
|
||||||
async_upnp_client: error
|
async_upnp_client: error
|
||||||
|
automower_ble: critical
|
||||||
|
|
||||||
recorder:
|
recorder:
|
||||||
purge_keep_days: 7
|
purge_keep_days: 7
|
||||||
@@ -110,7 +111,6 @@ cover:
|
|||||||
|
|
||||||
template: !include_dir_merge_list include/templates/
|
template: !include_dir_merge_list include/templates/
|
||||||
group: !include_dir_merge_named include/groups/
|
group: !include_dir_merge_named include/groups/
|
||||||
mqtt: !include include/mqtt.yaml
|
|
||||||
sensor: !include_dir_merge_list include/sensors/
|
sensor: !include_dir_merge_list include/sensors/
|
||||||
automation: !include_dir_merge_list include/automations/
|
automation: !include_dir_merge_list include/automations/
|
||||||
binary_sensor: !include_dir_merge_list include/binary_sensors/
|
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_select: !include_dir_merge_named include/input/select/
|
||||||
input_boolean: !include_dir_merge_named include/input/boolean/
|
input_boolean: !include_dir_merge_named include/input/boolean/
|
||||||
input_text: !include_dir_merge_named include/input/text/
|
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/
|
light: !include_dir_merge_list include/lights/
|
||||||
panel_iframe: !include_dir_merge_named include/panels/
|
panel_iframe: !include_dir_merge_named include/panels/
|
||||||
script: !include_dir_merge_named include/scripts/
|
script: !include_dir_merge_named include/scripts/
|
||||||
|
|||||||
@@ -33,275 +33,19 @@ cards:
|
|||||||
name: I morgen
|
name: I morgen
|
||||||
icon: mdi:briefcase-outline
|
icon: mdi:briefcase-outline
|
||||||
|
|
||||||
# 👨👩👧👦 Familien – tryk for at toggle syg/rask
|
# 👨👩👧👦 Familien
|
||||||
- type: grid
|
- type: glance
|
||||||
columns: 4
|
entities:
|
||||||
square: false
|
- entity: person.daniel_schusler_dethlefsen
|
||||||
cards:
|
|
||||||
|
|
||||||
- type: custom:button-card
|
|
||||||
entity: person.daniel_schusler_dethlefsen
|
|
||||||
name: Daniel
|
name: Daniel
|
||||||
show_name: true
|
- entity: person.claus_dethlefsen
|
||||||
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
|
|
||||||
name: Claus
|
name: Claus
|
||||||
show_name: true
|
- entity: person.anne_schusler_dethlefsen
|
||||||
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
|
|
||||||
name: Anne
|
name: Anne
|
||||||
show_name: true
|
- entity: person.andreas_schusler_dethlefsen
|
||||||
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
|
|
||||||
name: Andreas
|
name: Andreas
|
||||||
show_name: true
|
- entity: binary_sensor.family_presence
|
||||||
show_state: false
|
name: Familie
|
||||||
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
|
|
||||||
|
|
||||||
# 🪟 Gardiner
|
# 🪟 Gardiner
|
||||||
- type: grid
|
- type: grid
|
||||||
@@ -389,66 +133,31 @@ cards:
|
|||||||
icon: mdi:floor-plan
|
icon: mdi:floor-plan
|
||||||
tap_action:
|
tap_action:
|
||||||
action: call-service
|
action: call-service
|
||||||
service: script.turn_on
|
service: button.press
|
||||||
target:
|
target:
|
||||||
entity_id: script.roborock_manuelt_kokken
|
entity_id: button.roborock_s8_pro_ultra_kokken_bryggers
|
||||||
|
|
||||||
- type: button
|
- type: button
|
||||||
name: Syd
|
name: Syd
|
||||||
icon: mdi:floor-plan
|
icon: mdi:floor-plan
|
||||||
tap_action:
|
tap_action:
|
||||||
action: call-service
|
action: call-service
|
||||||
service: script.turn_on
|
service: button.press
|
||||||
target:
|
target:
|
||||||
entity_id: script.roborock_manuelt_syd
|
entity_id: button.roborock_s8_pro_ultra_syd
|
||||||
|
|
||||||
- type: button
|
- type: button
|
||||||
name: Mop
|
name: Mop
|
||||||
icon: mdi:floor-plan
|
icon: mdi:floor-plan
|
||||||
tap_action:
|
tap_action:
|
||||||
action: call-service
|
action: call-service
|
||||||
service: script.turn_on
|
service: button.press
|
||||||
target:
|
target:
|
||||||
entity_id: script.roborock_manuelt_mop
|
entity_id: button.roborock_s8_pro_ultra_vac_followed_by_mop
|
||||||
|
|
||||||
- type: custom:button-card
|
- type: button
|
||||||
entity: vacuum.roborock_s8_pro_ultra
|
name: Gå til dock
|
||||||
name: Start
|
icon: mdi:home-import-outline
|
||||||
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)
|
|
||||||
tap_action:
|
tap_action:
|
||||||
action: call-service
|
action: call-service
|
||||||
service: vacuum.return_to_base
|
service: vacuum.return_to_base
|
||||||
@@ -462,25 +171,8 @@ cards:
|
|||||||
entity: input_datetime.ploeneklipper_sidst_koert
|
entity: input_datetime.ploeneklipper_sidst_koert
|
||||||
show_icon: false
|
show_icon: false
|
||||||
show_name: true
|
show_name: true
|
||||||
show_state: false
|
show_state: true
|
||||||
show_label: true
|
|
||||||
name: Sidst klippet
|
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:
|
tap_action:
|
||||||
action: none
|
action: none
|
||||||
styles:
|
styles:
|
||||||
@@ -490,34 +182,30 @@ cards:
|
|||||||
- font-size: 11px
|
- font-size: 11px
|
||||||
- color: var(--secondary-text-color)
|
- color: var(--secondary-text-color)
|
||||||
- padding-bottom: 4px
|
- padding-bottom: 4px
|
||||||
label:
|
state:
|
||||||
- white-space: normal
|
- white-space: normal
|
||||||
- word-break: break-word
|
- word-break: break-word
|
||||||
- line-height: 1.2
|
- line-height: 1.2
|
||||||
- font-size: 13px
|
- font-size: 13px
|
||||||
- text-align: center
|
- text-align: center
|
||||||
|
|
||||||
- type: custom:button-card
|
- type: button
|
||||||
entity: lawn_mower.husqvarna_automower
|
|
||||||
name: Klip
|
name: Klip
|
||||||
icon: mdi:robot-mower
|
icon: mdi:robot-mower
|
||||||
tap_action:
|
tap_action:
|
||||||
action: call-service
|
action: call-service
|
||||||
service: script.turn_on
|
service: lawn_mower.start_mowing
|
||||||
target:
|
target:
|
||||||
entity_id: script.ploeneklipper_manuelt_start
|
entity_id: lawn_mower.husqvarna_automower
|
||||||
state:
|
|
||||||
- value: mowing
|
- type: button
|
||||||
name: Stop
|
name: Stop
|
||||||
styles:
|
icon: mdi:home-import-outline
|
||||||
card:
|
|
||||||
- background-color: rgba(255, 200, 0, 0.25)
|
|
||||||
- border: 1px solid rgba(255, 200, 0, 0.8)
|
|
||||||
tap_action:
|
tap_action:
|
||||||
action: call-service
|
action: call-service
|
||||||
service: script.turn_on
|
service: lawn_mower.dock
|
||||||
target:
|
target:
|
||||||
entity_id: script.ploeneklipper_manuelt_stop
|
entity_id: lawn_mower.husqvarna_automower
|
||||||
|
|
||||||
# 💡 Lys kontrol
|
# 💡 Lys kontrol
|
||||||
- type: horizontal-stack
|
- type: horizontal-stack
|
||||||
@@ -542,60 +230,17 @@ cards:
|
|||||||
action: more-info
|
action: more-info
|
||||||
show_state: true
|
show_state: true
|
||||||
|
|
||||||
- type: custom:button-card
|
- type: tile
|
||||||
entity: binary_sensor.garageport
|
entity: binary_sensor.garageport
|
||||||
name: Garage
|
name: Garage
|
||||||
show_name: true
|
features_position: bottom
|
||||||
show_state: false
|
vertical: 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)';
|
|
||||||
]]]
|
|
||||||
tap_action:
|
tap_action:
|
||||||
action: call-service
|
action: call-service
|
||||||
service: cover.toggle
|
service: cover.toggle
|
||||||
target:
|
target:
|
||||||
entity_id: cover.anne
|
entity_id: cover.anne
|
||||||
|
show_state: true
|
||||||
|
|
||||||
# 🎵 Sonos
|
# 🎵 Sonos
|
||||||
- type: grid
|
- type: grid
|
||||||
@@ -741,18 +386,6 @@ cards:
|
|||||||
action: call-service
|
action: call-service
|
||||||
service: script.tv_hygge_announcement
|
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
|
# 🗑️ Affald
|
||||||
- type: glance
|
- type: glance
|
||||||
columns: 3
|
columns: 3
|
||||||
@@ -897,6 +530,10 @@ cards:
|
|||||||
- entity: input_boolean.guests_mode
|
- entity: input_boolean.guests_mode
|
||||||
name: Vi har gæster
|
name: Vi har gæster
|
||||||
icon: mdi:account-group
|
icon: mdi:account-group
|
||||||
|
- entity: input_boolean.vacation_mode
|
||||||
|
name: 🌴 Vacation Mode
|
||||||
|
- entity: input_datetime.vacation_end
|
||||||
|
name: Slutter
|
||||||
|
|
||||||
- type: conditional
|
- type: conditional
|
||||||
conditions:
|
conditions:
|
||||||
|
|||||||
@@ -99,21 +99,3 @@ cards:
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
{{ ns.rows }}
|
{{ 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
|
- type: grid
|
||||||
cards:
|
cards:
|
||||||
- type: gauge
|
- type: gauge
|
||||||
entity: sensor.fjernvarme_ventil_anbefalet
|
entity: sensor.fjernvarme_ventil_3_ugers_gennemsnit
|
||||||
name: Anbefalet ventilposition (1–5)
|
name: Anbefalet ventilposition – 3 ugers snit (1–5)
|
||||||
min: 1
|
min: 1
|
||||||
max: 5
|
max: 5
|
||||||
needle: true
|
needle: true
|
||||||
@@ -412,9 +412,11 @@ sections:
|
|||||||
|
|
||||||
- type: markdown
|
- type: markdown
|
||||||
content: |-
|
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:
|
Gælder for begge manuelle hoveddrejehaner:
|
||||||
- Roth-fordeler (sauna)
|
- 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 |
|
| 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.
|
**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
|
- id: badevaerelse_motion_lys
|
||||||
alias: Badeværelse lys via bevægelse
|
alias: Badeværelse lys via bevægelse
|
||||||
mode: restart
|
mode: restart
|
||||||
|
|||||||
@@ -24,7 +24,8 @@
|
|||||||
{% set t = now().strftime('%H%M') | int %}
|
{% set t = now().strftime('%H%M') | int %}
|
||||||
{% if 600 <= t < 1600 %}morgen
|
{% if 600 <= t < 1600 %}morgen
|
||||||
{% elif 1600 <= t < 1900 %}eftermiddag
|
{% elif 1600 <= t < 1900 %}eftermiddag
|
||||||
{% elif 1900 <= t %}aften
|
{% elif 1900 <= t < 2100 %}aften_lys
|
||||||
|
{% elif 2100 <= t %}aften
|
||||||
{% else %}nat{% endif %}
|
{% else %}nat{% endif %}
|
||||||
timeout_min: >
|
timeout_min: >
|
||||||
{% set t = now().strftime('%H%M') | int %}
|
{% set t = now().strftime('%H%M') | int %}
|
||||||
@@ -32,7 +33,9 @@
|
|||||||
{{ states('input_number.stue_timeout_morgen') | int }}
|
{{ states('input_number.stue_timeout_morgen') | int }}
|
||||||
{% elif 1600 <= t < 1900 %}
|
{% elif 1600 <= t < 1900 %}
|
||||||
{{ states('input_number.stue_timeout_eftermiddag') | int }}
|
{{ 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 }}
|
{{ states('input_number.stue_timeout_aften') | int }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ states('input_number.stue_timeout_nat') | int }}
|
{{ states('input_number.stue_timeout_nat') | int }}
|
||||||
@@ -50,6 +53,15 @@
|
|||||||
{{ states('sensor.stue_belysningsstyrke') | int < lux_limit }}
|
{{ states('sensor.stue_belysningsstyrke') | int < lux_limit }}
|
||||||
sequence:
|
sequence:
|
||||||
- choose:
|
- 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:
|
- conditions:
|
||||||
- condition: template
|
- condition: template
|
||||||
value_template: "{{ dagperiode == 'morgen' }}"
|
value_template: "{{ dagperiode == 'morgen' }}"
|
||||||
@@ -64,6 +76,13 @@
|
|||||||
- service: scene.turn_on
|
- service: scene.turn_on
|
||||||
target:
|
target:
|
||||||
entity_id: scene.stue_annes_favorit
|
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:
|
default:
|
||||||
- service: scene.turn_on
|
- service: scene.turn_on
|
||||||
target:
|
target:
|
||||||
@@ -76,7 +95,7 @@
|
|||||||
id: motion_off
|
id: motion_off
|
||||||
- condition: template
|
- condition: template
|
||||||
value_template: >
|
value_template: >
|
||||||
{{ dagperiode != 'aften' or
|
{{ dagperiode not in ('aften','aften_lys') or
|
||||||
is_state('media_player.samsung_s95ca_55_3', 'off') }}
|
is_state('media_player.samsung_s95ca_55_3', 'off') }}
|
||||||
sequence:
|
sequence:
|
||||||
- delay:
|
- delay:
|
||||||
@@ -86,7 +105,7 @@
|
|||||||
state: "off"
|
state: "off"
|
||||||
- condition: template
|
- condition: template
|
||||||
value_template: >
|
value_template: >
|
||||||
{{ dagperiode != 'aften' or
|
{{ dagperiode not in ('aften','aften_lys') or
|
||||||
is_state('media_player.samsung_s95ca_55_3', 'off') }}
|
is_state('media_player.samsung_s95ca_55_3', 'off') }}
|
||||||
- service: light.turn_off
|
- service: light.turn_off
|
||||||
target:
|
target:
|
||||||
@@ -97,7 +116,7 @@
|
|||||||
- condition: trigger
|
- condition: trigger
|
||||||
id: tv_off
|
id: tv_off
|
||||||
- condition: template
|
- condition: template
|
||||||
value_template: "{{ dagperiode == 'aften' }}"
|
value_template: "{{ dagperiode in ('aften','aften_lys') }}"
|
||||||
sequence:
|
sequence:
|
||||||
- delay:
|
- delay:
|
||||||
minutes: "{{ timeout_min }}"
|
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
|
- id: mealie_generate_bilka_checklist_wednesday
|
||||||
alias: "Mealie indkøbsliste - onsdag morgen"
|
alias: "Mealie indkøbsliste - onsdag morgen"
|
||||||
trigger:
|
trigger:
|
||||||
@@ -19,3 +9,14 @@
|
|||||||
- wed
|
- wed
|
||||||
action:
|
action:
|
||||||
- service: script.mealie_shopping_refresh
|
- 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:
|
target:
|
||||||
entity_id: button.roborock_s8_pro_ultra_kokken_bryggers
|
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:
|
- choose:
|
||||||
- conditions:
|
- conditions:
|
||||||
- condition: state
|
- condition: template
|
||||||
entity_id: vacuum.roborock_s8_pro_ultra
|
value_template: "{{ wait.completed }}"
|
||||||
state: "cleaning"
|
|
||||||
sequence:
|
sequence:
|
||||||
- service: input_number.increment
|
- service: input_number.increment
|
||||||
target:
|
target:
|
||||||
@@ -109,17 +109,14 @@
|
|||||||
}} min.
|
}} min.
|
||||||
|
|
||||||
- conditions:
|
- conditions:
|
||||||
- condition: not
|
- condition: template
|
||||||
conditions:
|
value_template: "{{ not wait.completed }}"
|
||||||
- condition: state
|
|
||||||
entity_id: vacuum.roborock_s8_pro_ultra
|
|
||||||
state: "cleaning"
|
|
||||||
sequence:
|
sequence:
|
||||||
- service: notify.mobile_app_claus_iphone_15pro
|
- service: notify.mobile_app_claus_iphone_15pro
|
||||||
data:
|
data:
|
||||||
title: "⚠️ Roborock start fejlede"
|
title: "⚠️ Roborock start fejlede"
|
||||||
message: >
|
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') }}.
|
State: {{ states('vacuum.roborock_s8_pro_ultra') }}.
|
||||||
Status: {{ state_attr('vacuum.roborock_s8_pro_ultra', 'status') | default('ukendt', true) }}.
|
Status: {{ state_attr('vacuum.roborock_s8_pro_ultra', 'status') | default('ukendt', true) }}.
|
||||||
Error: {{ state_attr('vacuum.roborock_s8_pro_ultra', 'error') | default('ingen', 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
|
id: varme_vindue_trigger
|
||||||
description: "Kalder varme_recalculate når et vindue eller terrassedøren skifter tilstand"
|
description: "Kalder varme_recalculate når et vindue eller terrassedøren skifter tilstand"
|
||||||
mode: queued
|
mode: queued
|
||||||
max: 3
|
max: 10
|
||||||
trigger:
|
trigger:
|
||||||
- platform: state
|
- platform: state
|
||||||
entity_id:
|
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
|
max: 99999
|
||||||
step: 1
|
step: 1
|
||||||
mode: box
|
mode: box
|
||||||
initial: -1
|
initial: 67
|
||||||
|
|
||||||
shelly_fordor_event_cnt:
|
shelly_fordor_event_cnt:
|
||||||
name: Shelly fordoer event count
|
name: Shelly fordoer event count
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ varme_komfort_sovevaerelse:
|
|||||||
max: 28
|
max: 28
|
||||||
step: 0.5
|
step: 0.5
|
||||||
unit_of_measurement: "°C"
|
unit_of_measurement: "°C"
|
||||||
initial: 18
|
initial: 20
|
||||||
icon: mdi:thermometer
|
icon: mdi:thermometer
|
||||||
|
|
||||||
varme_komfort_kontor:
|
varme_komfort_kontor:
|
||||||
@@ -32,7 +32,7 @@ varme_komfort_kontor:
|
|||||||
max: 28
|
max: 28
|
||||||
step: 0.5
|
step: 0.5
|
||||||
unit_of_measurement: "°C"
|
unit_of_measurement: "°C"
|
||||||
initial: 19
|
initial: 20
|
||||||
icon: mdi:thermometer
|
icon: mdi:thermometer
|
||||||
|
|
||||||
varme_komfort_gang:
|
varme_komfort_gang:
|
||||||
@@ -41,7 +41,7 @@ varme_komfort_gang:
|
|||||||
max: 28
|
max: 28
|
||||||
step: 0.5
|
step: 0.5
|
||||||
unit_of_measurement: "°C"
|
unit_of_measurement: "°C"
|
||||||
initial: 19
|
initial: 20
|
||||||
icon: mdi:thermometer
|
icon: mdi:thermometer
|
||||||
|
|
||||||
varme_komfort_forgang:
|
varme_komfort_forgang:
|
||||||
@@ -68,7 +68,7 @@ varme_komfort_badevarelse:
|
|||||||
max: 28
|
max: 28
|
||||||
step: 0.5
|
step: 0.5
|
||||||
unit_of_measurement: "°C"
|
unit_of_measurement: "°C"
|
||||||
initial: 20
|
initial: 24.5
|
||||||
icon: mdi:thermometer
|
icon: mdi:thermometer
|
||||||
|
|
||||||
varme_komfort_stue:
|
varme_komfort_stue:
|
||||||
|
|||||||
@@ -4,5 +4,5 @@ mealie_shopping_refresh:
|
|||||||
- service: shell_command.mealie_shopping_merge
|
- service: shell_command.mealie_shopping_merge
|
||||||
- service: notify.mobile_app_claus_iphone_15pro
|
- service: notify.mobile_app_claus_iphone_15pro
|
||||||
data:
|
data:
|
||||||
title: "Bilka ToGo liste opdateret"
|
title: "Indkøbsliste opdateret i Mealie"
|
||||||
message: "Mealie-indkøb (fredag til torsdag) er flettet med Keep-basislisten."
|
message: "Indkøbslisten 'Bilka ToGo' er opdateret med opskrifter fra fredag til torsdag."
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ overvaagning:
|
|||||||
device_id: cf4f218aae515c84aea9f37f190dcfd5
|
device_id: cf4f218aae515c84aea9f37f190dcfd5
|
||||||
enabled: true
|
enabled: true
|
||||||
action: camera.snapshot
|
action: camera.snapshot
|
||||||
|
- action: homeassistant.update_entity
|
||||||
|
data:
|
||||||
|
entity_id: camera.indkorsel_snapshot
|
||||||
|
enabled: true
|
||||||
- delay:
|
- delay:
|
||||||
hours: 0
|
hours: 0
|
||||||
minutes: 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)
|
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)
|
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)
|
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)
|
|
||||||
|
|
||||||
print(
|
print(
|
||||||
'OK: '
|
'OK: '
|
||||||
|
|||||||
@@ -18,92 +18,83 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>🛒 Bilka ToGo</h1>
|
<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>
|
<table>
|
||||||
<tr><th colspan="2" class="cat">Andet</th></tr>
|
<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 dl hvidvin</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 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 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 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 knivspids muskatnød, fintrevet</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 knivspids røget paprika</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 knivspids sød paprika</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 smør, til stegning</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 squash, groftrevet</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 tsk sød paprika</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 tsk tørret timian</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>100 g rejer</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>100 g stenbiderrogn</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>2 spsk kapers (valgfrit)</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>2 spsk smør</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>2 tsk tørret oregano</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>200 g lasagneplader</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>3 dl mælk</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>4 grønne asparges</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>4 gulerødder, groftrevet</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>4 hvide asparges</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>4 skiver brød</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>5 stængler bladselleri, groftrevet</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>50 g rasp</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>75 g smør</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>8 rødspættefilet</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><th colspan="2" class="cat">Frost</th></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 spsk mayonnaise</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>2,50 dl piskefløde</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><th colspan="2" class="cat">Frugt & Grønt</th></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 fintrevet skal</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 citron, 1 spsk saft herfra</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>1 citron, skåret i både</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>12 skiver agurk</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>2 banan, skåret i skiver på ca. 1 cm</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>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><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 dl mild chilipasta</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 spsk hvedemel</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 spsk olivenolie, til stegning</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 spsk smør eller olie til stegning</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 håndfulde frisk dild, til pynt</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 finvalset havregryn</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 hvedemel</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 spsk olivenolie</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 dl basmati ris</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>3 spsk olivenolie, til stegning</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>50 g saltede peanuts</td></tr>
|
||||||
<tr><td class="cb"><input type="checkbox"></td><td>600g basmatiris</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>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>salt og friskkværnet peber</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><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><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 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>2 løg, 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>2 spsk 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>2 spsk persille, finthakket</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>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>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><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>
|
</table>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
{
|
{
|
||||||
"count": 78,
|
"count": 69,
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"name": "0,50 tsk chiliflager",
|
"name": "0,50 dl hvidvin",
|
||||||
"category": "andet"
|
"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"
|
"category": "andet"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -14,7 +22,11 @@
|
|||||||
"category": "andet"
|
"category": "andet"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "1 dl hvidvin",
|
"name": "1 dl mælk",
|
||||||
|
"category": "andet"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "1 dl rødvin, eller grøntsagsboullion",
|
||||||
"category": "andet"
|
"category": "andet"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -22,39 +34,23 @@
|
|||||||
"category": "andet"
|
"category": "andet"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "1 iceberg",
|
"name": "1 knivspids muskatnød, fintrevet",
|
||||||
"category": "andet"
|
"category": "andet"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "1 rødløg, i tynde ringe",
|
"name": "1 knivspids røget paprika",
|
||||||
"category": "andet"
|
"category": "andet"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "1 spsk lage fra de syltede cornichoner",
|
"name": "1 knivspids sød paprika",
|
||||||
"category": "andet"
|
"category": "andet"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "1 spsk sennep, - gerne sød",
|
"name": "1 spsk smør, til stegning",
|
||||||
"category": "andet"
|
"category": "andet"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "1 spsk sesamfrø",
|
"name": "1 squash, groftrevet",
|
||||||
"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",
|
|
||||||
"category": "andet"
|
"category": "andet"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -62,223 +58,179 @@
|
|||||||
"category": "andet"
|
"category": "andet"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "100 g cheddar",
|
"name": "1 tsk tørret timian",
|
||||||
"category": "andet"
|
"category": "andet"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "1,2 kg bagekartofler",
|
"name": "100 g rejer",
|
||||||
"category": "andet"
|
"category": "andet"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "1,2 liter vand",
|
"name": "100 g stenbiderrogn",
|
||||||
"category": "andet"
|
"category": "andet"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "1/2 tsk chiliflager",
|
"name": "2 spsk kapers (valgfrit)",
|
||||||
"category": "andet"
|
"category": "andet"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "1/2 tsk paprika",
|
"name": "2 spsk smør",
|
||||||
"category": "andet"
|
"category": "andet"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "1,50 stødt spidskommen",
|
"name": "2 tsk tørret oregano",
|
||||||
"category": "andet"
|
"category": "andet"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "1,50 tsk sød paprika",
|
"name": "200 g lasagneplader",
|
||||||
"category": "andet"
|
"category": "andet"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "2 dl grøntsagsbouillon",
|
"name": "3 dl mælk",
|
||||||
"category": "andet"
|
"category": "andet"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "2 fed hvidlag (flaekket)",
|
"name": "4 grønne asparges",
|
||||||
"category": "andet"
|
"category": "andet"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "2 fed hvidlag presset (til marinade)",
|
"name": "4 gulerødder, groftrevet",
|
||||||
"category": "andet"
|
"category": "andet"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "2 skalottelag (finthakkede)",
|
"name": "4 hvide asparges",
|
||||||
"category": "andet"
|
"category": "andet"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "2 spsk smaor",
|
"name": "4 skiver brød",
|
||||||
"category": "andet"
|
"category": "andet"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "2 spsk tikka masala paste",
|
"name": "5 stængler bladselleri, groftrevet",
|
||||||
"category": "andet"
|
"category": "andet"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "2 spsk toervin (hvidvin)",
|
"name": "50 g rasp",
|
||||||
"category": "andet"
|
"category": "andet"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "2 store lag (finthakkede)",
|
"name": "75 g smør",
|
||||||
"category": "andet"
|
"category": "andet"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "20 g cornichoner, meget finthakkede",
|
"name": "8 rødspættefilet",
|
||||||
"category": "andet"
|
"category": "andet"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "200 g squash, groftrevet",
|
"name": "2 spsk mayonnaise",
|
||||||
"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",
|
|
||||||
"category": "frost"
|
"category": "frost"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "2 dl piskefloede",
|
"name": "2,50 dl piskefløde",
|
||||||
"category": "frost"
|
"category": "frost"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "200g smaor (til bearnaise)",
|
"name": "1 citron – saft og fintrevet skal",
|
||||||
"category": "frost"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "50 g mayonnaise",
|
|
||||||
"category": "frost"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "0,50 agurk, i skiver",
|
|
||||||
"category": "frugt & grønt"
|
"category": "frugt & grønt"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "1 citron (saft og skal)",
|
"name": "1 citron, 1 spsk saft herfra",
|
||||||
"category": "frugt & grønt"
|
"category": "frugt & grønt"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "1 spsk tomatpure",
|
"name": "1 citron, skåret i både",
|
||||||
"category": "frugt & grønt"
|
"category": "frugt & grønt"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "2 daaser hakkede tomater (a 400g)",
|
"name": "12 skiver agurk",
|
||||||
"category": "frugt & grønt"
|
"category": "frugt & grønt"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "200g cherrytomater (halverede)",
|
"name": "2 banan, skåret i skiver på ca. 1 cm",
|
||||||
"category": "frugt & grønt"
|
"category": "frugt & grønt"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "4 tomater, i tern",
|
"name": "2 spsk koncentreret tomatpuré",
|
||||||
"category": "frugt & grønt"
|
"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"
|
"category": "kolonial"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "1 spsk olivenolie",
|
"name": "1 spsk hvedemel",
|
||||||
"category": "kolonial"
|
"category": "kolonial"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "1 tsk frisk ingefaer revet (til marinade)",
|
"name": "1 spsk olivenolie, til stegning",
|
||||||
"category": "kolonial"
|
"category": "kolonial"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "1 tsk salt",
|
"name": "1 spsk smør eller olie til stegning",
|
||||||
"category": "kolonial"
|
"category": "kolonial"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "1/2 bundt frisk estragon",
|
"name": "2 håndfulde frisk dild, til pynt",
|
||||||
"category": "kolonial"
|
"category": "kolonial"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "2 spsk hvidvinseddike",
|
"name": "2 spsk finvalset havregryn",
|
||||||
"category": "kolonial"
|
"category": "kolonial"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "2 spsk smaor (til ris)",
|
"name": "2 spsk hvedemel",
|
||||||
"category": "kolonial"
|
"category": "kolonial"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "2 spsk tandoorikrydderi",
|
"name": "2 spsk olivenolie",
|
||||||
"category": "kolonial"
|
"category": "kolonial"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "2 tsk frisk ingefaer revet (til sauce)",
|
"name": "3 dl basmati ris",
|
||||||
"category": "kolonial"
|
"category": "kolonial"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "3 spsk olivenolie",
|
"name": "3 spsk olivenolie, til stegning",
|
||||||
"category": "kolonial"
|
"category": "kolonial"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "4 kviste frisk timian",
|
"name": "50 g saltede peanuts",
|
||||||
"category": "kolonial"
|
"category": "kolonial"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "600g basmatiris",
|
"name": "Ris eller kartofler",
|
||||||
"category": "kolonial"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Frisk koriander til servering",
|
|
||||||
"category": "kolonial"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Salt",
|
|
||||||
"category": "kolonial"
|
"category": "kolonial"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "salt og friskkværnet peber",
|
"name": "salt og friskkværnet peber",
|
||||||
"category": "kolonial"
|
"category": "kolonial"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "Salt og hvid peber",
|
|
||||||
"category": "kolonial"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "Salt og peber",
|
"name": "Salt og peber",
|
||||||
"category": "kolonial"
|
"category": "kolonial"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "1 dl persille, finthakket",
|
"name": "1 fed hvidløg, finthakket",
|
||||||
"category": "kød & fisk"
|
"category": "kød & fisk"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -286,19 +238,23 @@
|
|||||||
"category": "kød & fisk"
|
"category": "kød & fisk"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "1 skalottelag (finthakket)",
|
"name": "2 løg, finthakket",
|
||||||
"category": "kød & fisk"
|
"category": "kød & fisk"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "1/2 bundt frisk persille (hakket)",
|
"name": "2 spsk frisk persille, hakket",
|
||||||
"category": "kød & fisk"
|
"category": "kød & fisk"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "1,2 kg kyllingebryst (i mundrette stykker)",
|
"name": "2 spsk persille, finthakket",
|
||||||
"category": "kød & fisk"
|
"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"
|
"category": "kød & fisk"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -306,11 +262,19 @@
|
|||||||
"category": "kød & fisk"
|
"category": "kød & fisk"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "50 g cornichoner, finthakket",
|
"name": "500 g hakket svinekød",
|
||||||
"category": "kød & fisk"
|
"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"
|
"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 |