Compare commits

...

44 Commits

Author SHA1 Message Date
claus 644cb28e88 Zigbee: opdater husplan med LQI-målinger 20/5 og nye enheder 2026-05-20 20:04:45 +02:00
claus 7cae5a8c8b Varme: gem defaults efter HA genstart 2026.5.1 2026-05-20 18:29:10 +02:00
claus 31c1258760 TODO: Gitea HTTPS via gitea.anneclaus.dk markeret løst 2026-05-20 17:34:42 +02:00
claus 7cb07af473 TODO: nas.anneclaus.dk via NPM markeret løst 2026-05-19 21:39:24 +02:00
claus 25ab4ceeef Stue lys: brug annes_favorit også i aften-perioden (efter 21:00) 2026-05-19 21:05:44 +02:00
claus ba3ba34db8 TODO: Plex markeret som skip 2026-05-19 21:00:18 +02:00
claus c54f583968 Varme: tilføj Bryggers og Køkken sektioner (uden climate-entiteter endnu) 2026-05-19 20:40:57 +02:00
claus 27224a4115 Madplan: opdater Mealie-links til https://mealie.anneclaus.dk 2026-05-18 18:22:03 +02:00
claus ff8324af1f TODO: Mealie HTTPS via NPM markeret løst 2026-05-18 18:19:38 +02:00
claus 6731257b83 Varme: tilføj havesensor inde til Kontor temperatur-graf 2026-05-18 07:29:03 +02:00
claus 8b533ca95a Dokumenter: tilføj zigbee husplan med enhedsplacering og LQI-data 2026-05-17 18:04:29 +02:00
claus b46d820c20 Madplan: tilføj store volumen op/ned knapper til Sonos Køkken 2026-05-17 17:45:26 +02:00
claus f5f085f155 Madplan: tilføj Claus Daily Mix 1-6 knapper - alle 4 familiers daily mix nu tilstede 2026-05-17 17:26:12 +02:00
claus 63b6283fdb Madplan: ret Sonos-kildenavne - Claus→Anne Daily Mix, fjern kommentarer 2026-05-17 17:23:49 +02:00
claus 9e446e0d57 Madplan: tilføj Sonos Køkken media-control + 22 kildeknapper (P3, Family Mix, Danske fav, Rock klassikere, Claus/Andreas/Daniel Daily Mix 1-6) 2026-05-17 17:21:07 +02:00
claus d0e3a6c3e5 Indkørsel: erstat Hue plug med Zigbee stik_indkorsel
- grupper.yaml: fjern light.indkorsel_plug fra Indkørsel og Udendørslamper grupper
- lysindkorsel.yaml: tilføj switch.stik_indkorsel turn_on/off ved alle 6 handlinger
- presence_simulation.yaml: tilføj switch.stik_indkorsel i morgen/aften sekvenser
- andreas_kommer_hjem: tilføj switch.stik_indkorsel turn_on/off
2026-05-17 11:14:38 +02:00
claus 8b2ff535de TODO: marker MQTT og FTP som lukket på router 2026-05-17 07:59:39 +02:00
claus acdc7259a7 TODO: tilføj MQTT og SSH til port forward sikringsliste 2026-05-17 07:57:31 +02:00
claus c297012592 TODO: tilføj opgaver for sikring af port forwards via NPM 2026-05-17 07:54:28 +02:00
claus 52dc97dd3d HA: external_url skiftet til ha.anneclaus.dk 2026-05-17 07:43:28 +02:00
claus 532f42b310 TODO: marker home_charging og Google AI MAX_TOKENS som løst 2026-05-16 20:04:26 +02:00
claus ad5bdf91b9 TODO: Husqvarna Automower BLE markeret løst 2026-05-16 20:02:23 +02:00
claus 0cb3145257 gitignore: ignorer www/snapshots - fjern fra tracking 2026-05-16 19:44:10 +02:00
claus 54144a03b8 TODO: markér aiohttp/400 løst — NPM HTTPS reverse proxy sat op 2026-05-16 19:42:57 +02:00
claus af3779a573 HA: HTTPS via NPM - opdater external_url og trusted_proxies 2026-05-16 18:05:10 +02:00
claus 3094620985 Nginx: porte ændret til 10080/10443 (8080/8443 optaget af Java/Synology) 2026-05-16 16:20:48 +02:00
claus 8057b3abf1 Nginx: use ports 8080/8443 to avoid conflict with Synology DSM on port 443 2026-05-16 16:12:22 +02:00
claus 677debfc27 Nginx: add nginx-proxy-manager service to docker-compose.infrastructure.yml 2026-05-16 16:08:12 +02:00
claus 910de6ed54 Varme: fix shell_command - run python3 directly inside container 2026-05-16 12:45:35 +02:00
claus bc6799c126 Varme: gem komforttemperaturer som defaults via knap på dashboard 2026-05-16 11:59:23 +02:00
claus 9f9de0524a Vanding: fix nedbørsprognose via trigger-template sensor (HA 2024.3+ forecast API) 2026-05-16 11:49:21 +02:00
claus eacd137a7c Gallery: add touch swipe support for iPhone/iPad 2026-05-16 10:37:10 +02:00
claus 63288dbb4b Gallery: add prune button (behold 100), webhook automation, shell_command 2026-05-16 10:35:28 +02:00
claus c394d6b974 Gallery: fix iframe cache-bust by versioning loader URL in dashboard YAML 2026-05-16 10:27:23 +02:00
claus fa64223630 Gallery: show all 192 snapshots (MAX 500), fix shell_command to use docker exec 2026-05-16 10:01:11 +02:00
claus f2ac6064b5 Dashboard views reorganized, mealie/roborock automations, indkorsel snapshots, wavin/sonoff docs, varme/sikkerhed updates 2026-05-16 07:28:28 +02:00
claus bd134bafef docs: tilføj vildtkamera (Reolink Argus 4 Pro + solpanel) på ønskeliste; script vi_laver_mad udvides med Daniel 2026-05-10 19:26:41 +02:00
claus 4e0819d4ff docs: udvid Aqara vindues/dørsensorer fra 6 til 15 stk (forgang, køkken, bryggers, stue, fordør, bagdør, kontor) 2026-05-10 17:48:05 +02:00
claus 0d112fb4d0 docs: tilføj automower BLE fejlfinding til kontekst 2026-05-10 16:16:19 +02:00
claus 067d5c6a63 Vi laver mad script+knap, Ally online, TODO/oensker opdateret, mealie/bilka sync 2026-05-09 06:19:50 +02:00
claus 5a7d25fd3c TODO: tilføj automower BLE auth fejl og AI MAX_TOKENS 2026-05-07 07:14:07 +02:00
claus bee2028f0b Julelys kun i vintersæson uge 42-8, tilføj TODO liste 2026-05-06 07:35:07 +02:00
claus 1525cc0070 Garageport: brug zigbee binary_sensor, animation baseret på last_changed 2026-05-05 19:43:51 +02:00
claus 812199889e Tilføj docker-compose, Touchline manual, netværksgenstart dok, fix ._* ignore 2026-05-05 19:40:05 +02:00
48 changed files with 2730 additions and 578 deletions
+1 -1
View File
@@ -1 +1 @@
2026.4.4 2026.5.1
+5
View File
@@ -38,6 +38,10 @@ configuration_minimal.yaml
!.HA_VERSION !.HA_VERSION
!customize !customize
# --- Re-ignore macOS metadata files inside whitelisted dirs ---
include/**/.DS_Store
include/**/._*
# --- Whitelist directories --- # --- Whitelist directories ---
!www/ !www/
!include/ !include/
@@ -66,6 +70,7 @@ configuration_minimal.yaml
/oldscripts.yaml /oldscripts.yaml
# --- Local media snapshots and downloads --- # --- Local media snapshots and downloads ---
/www/snapshots/
/www/affalddk/ /www/affalddk/
/www/community/ /www/community/
/www/indkorsel_snapshot.jpg /www/indkorsel_snapshot.jpg
+3 -3
View File
@@ -6,7 +6,7 @@ default_config:
homeassistant: homeassistant:
name: !secret name name: !secret name
external_url: "http://anneclaus.duckdns.org:8123" external_url: "https://ha.anneclaus.dk"
internal_url: "http://dethlefsen:8123" internal_url: "http://dethlefsen:8123"
auth_providers: auth_providers:
- type: homeassistant - type: homeassistant
@@ -27,6 +27,7 @@ http:
trusted_proxies: trusted_proxies:
- 127.0.0.1 - 127.0.0.1
- 10.0.0.142 - 10.0.0.142
- 172.17.0.0/16 # Docker bridge (NPM)
logger: logger:
default: warning default: warning
@@ -38,6 +39,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 +112,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 +121,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/
+37 -402
View File
@@ -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,62 +230,17 @@ cards:
action: more-info action: more-info
show_state: true show_state: true
- type: custom:button-card - type: tile
entity: cover.anne entity: binary_sensor.garageport
name: Garage name: Garage
show_name: true features_position: bottom
show_state: false vertical: false
show_label: true
label: >
[[[
const s = entity.state;
if (s === 'opening') return 'Åbner...';
if (s === 'closing') return 'Lukker...';
if (s === 'open') return 'Åben';
return 'Lukket';
]]]
icon: >
[[[
const s = entity.state;
if (s === 'open' || s === 'opening') return 'mdi:garage-open-variant';
return '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 s = entity.state;
return (s === 'opening' || s === 'closing')
? 'garage-pulse 0.8s ease-in-out infinite'
: 'none';
]]]
icon:
- color: >
[[[
const s = entity.state;
if (s === 'open') return 'orange';
if (s === 'opening' || s === 'closing') return 'dodgerblue';
return 'var(--primary-text-color)';
]]]
label:
- font-size: 11px
- color: >
[[[
const s = entity.state;
if (s === 'opening' || s === 'closing') return 'dodgerblue';
return '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
@@ -743,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
@@ -899,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:
+603 -19
View File
@@ -18,9 +18,9 @@ cards:
[[[ [[[
var slug = states['sensor.dagens_aftensmad_slug'].state; var slug = states['sensor.dagens_aftensmad_slug'].state;
if (slug && slug !== '' && slug !== 'unknown') { if (slug && slug !== '' && slug !== 'unknown') {
return 'http://anneclaus.dk:9925/g/home/r/' + slug; return 'https://mealie.anneclaus.dk/g/home/r/' + slug;
} }
return 'http://anneclaus.dk:9925'; return 'https://mealie.anneclaus.dk';
]]] ]]]
styles: styles:
card: card:
@@ -41,7 +41,7 @@ cards:
- color: white - color: white
- padding-top: 4px - padding-top: 4px
# 🎵 Musik i køkken + Der er mad # 🎵 Musik i køkken + Vi laver mad + Der er mad
- type: grid - type: grid
columns: 2 columns: 2
square: false square: false
@@ -57,6 +57,13 @@ cards:
data: data:
source: "1 Family Mix" source: "1 Family Mix"
- type: button
name: Vi laver mad
icon: mdi:chef-hat
tap_action:
action: call-service
service: script.vi_laver_mad
- type: button - type: button
name: Der er mad! name: Der er mad!
icon: mdi:silverware-fork-knife icon: mdi:silverware-fork-knife
@@ -79,7 +86,7 @@ cards:
{%- set slug = recipe.slug if recipe else '' -%} {%- set slug = recipe.slug if recipe else '' -%}
{%- set label = 'I dag' if offset == 0 else days[day.weekday()] -%} {%- set label = 'I dag' if offset == 0 else days[day.weekday()] -%}
{%- if slug -%} {%- if slug -%}
{%- set ns.rows = ns.rows + "| **" + label + "** | [" + name + "](http://anneclaus.dk:9925/g/home/r/" + slug + ") |\n" -%} {%- set ns.rows = ns.rows + "| **" + label + "** | [" + name + "](https://mealie.anneclaus.dk/g/home/r/" + slug + ") |\n" -%}
{%- elif name -%} {%- elif name -%}
{%- set ns.rows = ns.rows + "| **" + label + "** | " + name + " |\n" -%} {%- set ns.rows = ns.rows + "| **" + label + "** | " + name + " |\n" -%}
{%- else -%} {%- else -%}
@@ -92,21 +99,598 @@ cards:
| --- | --- | | --- | --- |
{{ ns.rows }} {{ ns.rows }}
# 🛒 Bilka ToGo - opdater og vis kryds-af liste # 🎵 Sonos Køkken
- type: vertical-stack - type: media-control
entity: media_player.kokken
name: Sonos Køkken
- type: grid
columns: 2
square: false
cards: cards:
- type: markdown - type: custom:button-card
content: | name: Volumen ned
## Bilka ToGo - kryds-af icon: mdi:volume-minus
Tryk på knappen for at hente ingredienser fra ugeplanen (fredagtorsdag).
- type: button
name: Opdater Bilka ToGo-liste nu
icon: mdi:cart-check
tap_action: tap_action:
action: call-service action: perform-action
service: script.mealie_shopping_refresh perform_action: media_player.volume_down
target:
entity_id: media_player.kokken
hold_action:
action: perform-action
perform_action: media_player.volume_down
target:
entity_id: media_player.kokken
styles:
card:
- height: 72px
- font-size: 16px
- background: var(--primary-color)
icon:
- color: white
- width: 36px
name:
- color: white
- font-size: 13px
- type: custom:button-card
name: Volumen op
icon: mdi:volume-plus
tap_action:
action: perform-action
perform_action: media_player.volume_up
target:
entity_id: media_player.kokken
hold_action:
action: perform-action
perform_action: media_player.volume_up
target:
entity_id: media_player.kokken
styles:
card:
- height: 72px
- font-size: 16px
- background: var(--primary-color)
icon:
- color: white
- width: 36px
name:
- color: white
- font-size: 13px
- type: grid
columns: 3
square: false
cards:
- type: custom:button-card
name: DR P3
icon: mdi:radio
tap_action:
action: perform-action
perform_action: media_player.select_source
target:
entity_id: media_player.kokken
data:
source: "0 DR P3"
styles:
card:
- height: 52px
- padding: 6px 8px
icon:
- width: 18px
name:
- font-size: 11px
- type: custom:button-card
name: Family Mix
icon: mdi:account-music
tap_action:
action: perform-action
perform_action: media_player.select_source
target:
entity_id: media_player.kokken
data:
source: "1 Family Mix"
styles:
card:
- height: 52px
- padding: 6px 8px
icon:
- width: 18px
name:
- font-size: 11px
- type: custom:button-card
name: Danske fav.
icon: mdi:music-note
tap_action:
action: perform-action
perform_action: media_player.select_source
target:
entity_id: media_player.kokken
data:
source: "Danske favoritter"
styles:
card:
- height: 52px
- padding: 6px 8px
icon:
- width: 18px
name:
- font-size: 11px
- type: custom:button-card
name: Rock klassikere
icon: mdi:music-note-outline
tap_action:
action: perform-action
perform_action: media_player.select_source
target:
entity_id: media_player.kokken
data:
source: "Danske rock klassikere"
styles:
card:
- height: 52px
- padding: 6px 8px
icon:
- width: 18px
name:
- font-size: 11px
- type: custom:button-card
name: Anne Mix 1
icon: mdi:playlist-music
tap_action:
action: perform-action
perform_action: media_player.select_source
target:
entity_id: media_player.kokken
data:
source: "Anne Daily Mix 1"
styles:
card:
- height: 52px
- padding: 6px 8px
icon:
- width: 18px
name:
- font-size: 11px
- type: custom:button-card
name: Anne Mix 2
icon: mdi:playlist-music
tap_action:
action: perform-action
perform_action: media_player.select_source
target:
entity_id: media_player.kokken
data:
source: "Anne Daily Mix 2"
styles:
card:
- height: 52px
- padding: 6px 8px
icon:
- width: 18px
name:
- font-size: 11px
- type: custom:button-card
name: Anne Mix 3
icon: mdi:playlist-music
tap_action:
action: perform-action
perform_action: media_player.select_source
target:
entity_id: media_player.kokken
data:
source: "Anne Daily Mix 3"
styles:
card:
- height: 52px
- padding: 6px 8px
icon:
- width: 18px
name:
- font-size: 11px
- type: custom:button-card
name: Anne Mix 4
icon: mdi:playlist-music
tap_action:
action: perform-action
perform_action: media_player.select_source
target:
entity_id: media_player.kokken
data:
source: "Anne Daily Mix 4"
styles:
card:
- height: 52px
- padding: 6px 8px
icon:
- width: 18px
name:
- font-size: 11px
- type: custom:button-card
name: Anne Mix 5
icon: mdi:playlist-music
tap_action:
action: perform-action
perform_action: media_player.select_source
target:
entity_id: media_player.kokken
data:
source: "Anne Daily Mix 5"
styles:
card:
- height: 52px
- padding: 6px 8px
icon:
- width: 18px
name:
- font-size: 11px
- type: custom:button-card
name: Anne Mix 6
icon: mdi:playlist-music
tap_action:
action: perform-action
perform_action: media_player.select_source
target:
entity_id: media_player.kokken
data:
source: "Anne Daily Mix 6"
styles:
card:
- height: 52px
- padding: 6px 8px
icon:
- width: 18px
name:
- font-size: 11px
- type: custom:button-card
name: Claus Mix 1
icon: mdi:playlist-music
tap_action:
action: perform-action
perform_action: media_player.select_source
target:
entity_id: media_player.kokken
data:
source: "Claus Daily Mix 1"
styles:
card:
- height: 52px
- padding: 6px 8px
icon:
- width: 18px
name:
- font-size: 11px
- type: custom:button-card
name: Claus Mix 2
icon: mdi:playlist-music
tap_action:
action: perform-action
perform_action: media_player.select_source
target:
entity_id: media_player.kokken
data:
source: "Claus Daily Mix 2"
styles:
card:
- height: 52px
- padding: 6px 8px
icon:
- width: 18px
name:
- font-size: 11px
- type: custom:button-card
name: Claus Mix 3
icon: mdi:playlist-music
tap_action:
action: perform-action
perform_action: media_player.select_source
target:
entity_id: media_player.kokken
data:
source: "Claus Daily Mix 3"
styles:
card:
- height: 52px
- padding: 6px 8px
icon:
- width: 18px
name:
- font-size: 11px
- type: custom:button-card
name: Claus Mix 4
icon: mdi:playlist-music
tap_action:
action: perform-action
perform_action: media_player.select_source
target:
entity_id: media_player.kokken
data:
source: "Claus Daily Mix 4"
styles:
card:
- height: 52px
- padding: 6px 8px
icon:
- width: 18px
name:
- font-size: 11px
- type: custom:button-card
name: Claus Mix 5
icon: mdi:playlist-music
tap_action:
action: perform-action
perform_action: media_player.select_source
target:
entity_id: media_player.kokken
data:
source: "Claus Daily Mix 5"
styles:
card:
- height: 52px
- padding: 6px 8px
icon:
- width: 18px
name:
- font-size: 11px
- type: custom:button-card
name: Claus Mix 6
icon: mdi:playlist-music
tap_action:
action: perform-action
perform_action: media_player.select_source
target:
entity_id: media_player.kokken
data:
source: "Claus Daily Mix 6"
styles:
card:
- height: 52px
- padding: 6px 8px
icon:
- width: 18px
name:
- font-size: 11px
- type: custom:button-card
name: Andreas Mix 1
icon: mdi:playlist-music
tap_action:
action: perform-action
perform_action: media_player.select_source
target:
entity_id: media_player.kokken
data:
source: "Andreas Daily Mix 1"
styles:
card:
- height: 52px
- padding: 6px 8px
icon:
- width: 18px
name:
- font-size: 11px
- type: custom:button-card
name: Andreas Mix 2
icon: mdi:playlist-music
tap_action:
action: perform-action
perform_action: media_player.select_source
target:
entity_id: media_player.kokken
data:
source: "Andreas Daily Mix 2"
styles:
card:
- height: 52px
- padding: 6px 8px
icon:
- width: 18px
name:
- font-size: 11px
- type: custom:button-card
name: Andreas Mix 3
icon: mdi:playlist-music
tap_action:
action: perform-action
perform_action: media_player.select_source
target:
entity_id: media_player.kokken
data:
source: "Andreas Daily Mix 3"
styles:
card:
- height: 52px
- padding: 6px 8px
icon:
- width: 18px
name:
- font-size: 11px
- type: custom:button-card
name: Andreas Mix 4
icon: mdi:playlist-music
tap_action:
action: perform-action
perform_action: media_player.select_source
target:
entity_id: media_player.kokken
data:
source: "Andreas Daily Mix 4"
styles:
card:
- height: 52px
- padding: 6px 8px
icon:
- width: 18px
name:
- font-size: 11px
- type: custom:button-card
name: Andreas Mix 5
icon: mdi:playlist-music
tap_action:
action: perform-action
perform_action: media_player.select_source
target:
entity_id: media_player.kokken
data:
source: "Andreas Daily Mix 5"
styles:
card:
- height: 52px
- padding: 6px 8px
icon:
- width: 18px
name:
- font-size: 11px
- type: custom:button-card
name: Andreas Mix 6
icon: mdi:playlist-music
tap_action:
action: perform-action
perform_action: media_player.select_source
target:
entity_id: media_player.kokken
data:
source: "Andreas Daily Mix 6"
styles:
card:
- height: 52px
- padding: 6px 8px
icon:
- width: 18px
name:
- font-size: 11px
- type: custom:button-card
name: Daniel Mix 1
icon: mdi:playlist-music
tap_action:
action: perform-action
perform_action: media_player.select_source
target:
entity_id: media_player.kokken
data:
source: "Daniel Daily Mix 1"
styles:
card:
- height: 52px
- padding: 6px 8px
icon:
- width: 18px
name:
- font-size: 11px
- type: custom:button-card
name: Daniel Mix 2
icon: mdi:playlist-music
tap_action:
action: perform-action
perform_action: media_player.select_source
target:
entity_id: media_player.kokken
data:
source: "Daniel Daily Mix 2"
styles:
card:
- height: 52px
- padding: 6px 8px
icon:
- width: 18px
name:
- font-size: 11px
- type: custom:button-card
name: Daniel Mix 3
icon: mdi:playlist-music
tap_action:
action: perform-action
perform_action: media_player.select_source
target:
entity_id: media_player.kokken
data:
source: "Daniel Daily Mix 3"
styles:
card:
- height: 52px
- padding: 6px 8px
icon:
- width: 18px
name:
- font-size: 11px
- type: custom:button-card
name: Daniel Mix 4
icon: mdi:playlist-music
tap_action:
action: perform-action
perform_action: media_player.select_source
target:
entity_id: media_player.kokken
data:
source: "Daniel Daily Mix 4"
styles:
card:
- height: 52px
- padding: 6px 8px
icon:
- width: 18px
name:
- font-size: 11px
- type: custom:button-card
name: Daniel Mix 5
icon: mdi:playlist-music
tap_action:
action: perform-action
perform_action: media_player.select_source
target:
entity_id: media_player.kokken
data:
source: "Daniel Daily Mix 5"
styles:
card:
- height: 52px
- padding: 6px 8px
icon:
- width: 18px
name:
- font-size: 11px
- type: custom:button-card
name: Daniel Mix 6
icon: mdi:playlist-music
tap_action:
action: perform-action
perform_action: media_player.select_source
target:
entity_id: media_player.kokken
data:
source: "Daniel Daily Mix 6"
styles:
card:
- height: 52px
- padding: 6px 8px
icon:
- width: 18px
name:
- font-size: 11px
- type: iframe
url: /local/bilka_togo_checklist.html
aspect_ratio: 100%
+215
View File
@@ -0,0 +1,215 @@
title: Vanding
path: vanding
icon: mdi:sprinkler-variant
type: sections
max_columns: 2
sections:
# 💧 Jordfugt målere
- type: grid
cards:
- type: heading
heading: Jordfugt
icon: mdi:water-percent
- type: gauge
entity: sensor.annes_havesensor_soil_moisture_1
name: Højbed 1 Ærter
min: 0
max: 100
needle: true
severity:
green: 40
yellow: 20
red: 0
- type: gauge
entity: sensor.annes_havesensor_soil_moisture_2
name: Højbed 2 Kartofler
min: 0
max: 100
needle: true
severity:
green: 40
yellow: 20
red: 0
- type: gauge
entity: sensor.annes_havesensor_soil_moisture_3
name: Højbed 3 Rabarber
min: 0
max: 100
needle: true
severity:
green: 40
yellow: 20
red: 0
- type: gauge
entity: sensor.annes_havesensor_soil_moisture_4
name: Drivhus
min: 0
max: 100
needle: true
severity:
green: 45
yellow: 25
red: 0
# 📈 Jordfugt historik
- type: grid
cards:
- type: heading
heading: Jordfugt 7 dage
icon: mdi:chart-line
- type: history-graph
title: Højbede (%)
entities:
- entity: sensor.annes_havesensor_soil_moisture_1
name: HB1 Ærter
- entity: sensor.annes_havesensor_soil_moisture_2
name: HB2 Kartofler
- entity: sensor.annes_havesensor_soil_moisture_3
name: HB3 Rabarber
hours_to_show: 168
refresh_interval: 900
- type: history-graph
title: Drivhus (%)
entities:
- entity: sensor.annes_havesensor_soil_moisture_4
name: Drivhus
hours_to_show: 168
refresh_interval: 900
# 🌧️ Regn & vejr
- type: grid
cards:
- type: heading
heading: Regn (Netatmo)
icon: mdi:weather-rainy
- type: tile
entity: sensor.n22_nedbor
name: Nedbør nu
- type: tile
entity: sensor.n22_precipitation_today
name: Nedbør i dag
- type: history-graph
title: Nedbør 7 dage
entities:
- entity: sensor.n22_precipitation_today
name: Nedbør
hours_to_show: 168
refresh_interval: 1800
- type: custom:apexcharts-card
header:
show: true
title: Forventet nedbør næste 7 dage
graph_span: 7d
span:
start: day
apex_config:
chart:
type: bar
height: 200
dataLabels:
enabled: true
formatter: |
EVAL:function(val) { return val ? val + ' mm' : ''; }
xaxis:
type: datetime
labels:
datetimeFormatter:
day: "dd/MM"
yaxis:
min: 0
title:
text: mm
series:
- entity: sensor.vejr_daglig_prognose
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
+96 -4
View File
@@ -159,6 +159,8 @@ sections:
series: series:
- entity: sensor.kontor_motion_temperatur - entity: sensor.kontor_motion_temperatur
name: Hue name: Hue
- entity: sensor.annes_havesensor_indoor_temperature
name: Havesensor inde
- entity: climate.kontor - entity: climate.kontor
attribute: current_temperature attribute: current_temperature
name: Roth aktuelt name: Roth aktuelt
@@ -357,6 +359,84 @@ sections:
curve: stepline curve: stepline
color: "#ff8800" color: "#ff8800"
- type: grid
cards:
- type: custom:apexcharts-card
graph_span: 24h
header:
show: true
title: Bryggers
show_states: true
colorize_states: true
now:
show: true
label: Nu
apex_config:
chart:
height: 240
grid:
strokeDashArray: 2
xaxis:
type: datetime
labels:
datetimeFormatter:
hour: HH:mm
yaxis:
decimalsInFloat: 1
tickAmount: 6
series:
- entity: sensor.temp_bryggers_temperatur
name: Temperatur
# TODO: tilføj climate-entity når tænd/sluk er monteret
# - entity: climate.bryggers
# attribute: current_temperature
# name: Roth aktuelt
# - entity: climate.bryggers
# attribute: temperature
# name: Roth mål
# stroke_width: 1
# curve: stepline
# color: "#ff8800"
- type: grid
cards:
- type: custom:apexcharts-card
graph_span: 24h
header:
show: true
title: Køkken
show_states: true
colorize_states: true
now:
show: true
label: Nu
apex_config:
chart:
height: 240
grid:
strokeDashArray: 2
xaxis:
type: datetime
labels:
datetimeFormatter:
hour: HH:mm
yaxis:
decimalsInFloat: 1
tickAmount: 6
series:
- entity: sensor.temp_kokken_temperatur
name: Temperatur
# TODO: tilføj climate-entity når tænd/sluk er monteret
# - entity: climate.kokken
# attribute: current_temperature
# name: Roth aktuelt
# - entity: climate.kokken
# attribute: temperature
# name: Roth mål
# stroke_width: 1
# curve: stepline
# color: "#ff8800"
# Indstillinger: Komforttemperaturer og sænkninger # Indstillinger: Komforttemperaturer og sænkninger
- type: grid - type: grid
cards: cards:
@@ -372,6 +452,9 @@ sections:
- entity: input_number.varme_komfort_lille_bad - entity: input_number.varme_komfort_lille_bad
- entity: input_number.varme_komfort_badevarelse - entity: input_number.varme_komfort_badevarelse
- entity: input_number.varme_komfort_stue - entity: input_number.varme_komfort_stue
# TODO: aktiver når climate-entiteter er oprettet
# - entity: input_number.varme_komfort_bryggers
# - entity: input_number.varme_komfort_kokken
- type: entities - type: entities
title: Sænkninger og ferie title: Sænkninger og ferie
@@ -389,12 +472,19 @@ sections:
action: call-service action: call-service
service: script.varme_recalculate service: script.varme_recalculate
- type: button
name: Gem temperaturer som standard
icon: mdi:content-save
tap_action:
action: perform-action
perform_action: script.varme_save_defaults
# Ventilposition # Ventilposition
- 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 (15) name: Anbefalet ventilposition 3 ugers snit (15)
min: 1 min: 1
max: 5 max: 5
needle: true needle: true
@@ -412,9 +502,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)
+352
View File
@@ -0,0 +1,352 @@
title: Sikkerhed
path: sikkerhed
icon: mdi:shield-home
cards:
# ─────────────────────────────────────────────────────────────
# 📷 KAMERAER
# ─────────────────────────────────────────────────────────────
- type: grid
columns: 2
square: false
cards:
- type: picture-entity
entity: camera.terrasse_sub
name: Terasse
camera_view: live
show_state: false
show_name: true
tap_action:
action: fire-dom-event
browser_mod:
service: browser_mod.popup
data:
title: Terasse Live
content:
type: vertical-stack
cards:
- type: picture-entity
entity: camera.terrasse_sub
camera_view: live
show_name: false
show_state: false
tap_action:
action: none
- type: tile
entity: number.terrasse_focus
name: Fokus
icon: mdi:focus-field
features:
- type: numeric-input
style: slider
- type: tile
entity: number.terrasse_zoom
name: Zoom
icon: mdi:magnify
features:
- type: numeric-input
style: slider
- type: picture-entity
entity: camera.indkoersel_sub
name: Indkørsel
camera_view: live
show_state: false
show_name: true
tap_action:
action: fire-dom-event
browser_mod:
service: browser_mod.popup
data:
title: Indkørsel Live
content:
type: vertical-stack
cards:
- type: picture-entity
entity: camera.indkoersel_sub
camera_view: live
show_name: false
show_state: false
tap_action:
action: none
- type: tile
entity: number.indkoersel_focus
name: Fokus
icon: mdi:focus-field
features:
- type: numeric-input
style: slider
- type: tile
entity: number.indkoersel_zoom
name: Zoom
icon: mdi:magnify
features:
- type: numeric-input
style: slider
# ─────────────────────────────────────────────────────────────
# 🛡️ SIKKERHEDSSTATUS
# ─────────────────────────────────────────────────────────────
- type: heading
heading: Sikkerhedsstatus
heading_style: title
- type: grid
columns: 2
square: false
cards:
# 👥 Tilstedeværelse
- type: custom:mushroom-template-card
entity: binary_sensor.family_presence
primary: >
{{ 'Nogen hjemme' if is_state('binary_sensor.family_presence', 'on') else 'Ingen hjemme' }}
secondary: >
{% set persons = [
('Claus', 'person.claus_dethlefsen'),
('Anne', 'person.anne_schusler_dethlefsen'),
('Andreas', 'person.andreas_schusler_dethlefsen'),
('Daniel', 'person.daniel_schusler_dethlefsen')
] %}
{% set ns = namespace(home=[]) %}
{% for name, eid in persons %}
{% if is_state(eid, 'home') %}{% set ns.home = ns.home + [name] %}{% endif %}
{% endfor %}
{{ ns.home | join(', ') if ns.home else 'Alle ude' }}
icon: >
{{ 'mdi:home-account' if is_state('binary_sensor.family_presence', 'on') else 'mdi:home-outline' }}
icon_color: >
{{ 'green' if is_state('binary_sensor.family_presence', 'on') else 'blue' }}
tap_action:
action: none
# 💡 Lys
- type: custom:mushroom-template-card
entity: light.alle_lys
primary: >
{{ 'Lys er tændt' if is_state('light.alle_lys', 'on') else 'Alt lys slukket' }}
secondary: ""
icon: >
{{ 'mdi:lightbulb-on' if is_state('light.alle_lys', 'on') else 'mdi:lightbulb-off' }}
icon_color: >
{{ 'yellow' if is_state('light.alle_lys', 'on') else 'grey' }}
tap_action:
action: navigate
navigation_path: /lovelace/lys
# 🪟 Vinduer og terrassedør
- type: custom:mushroom-template-card
multiline_secondary: true
primary: >
{% set sensors = [
'binary_sensor.andreas_vindue',
'binary_sensor.daniel_vindue',
'binary_sensor.sovevaerelse_vindue',
'binary_sensor.badevaerelse_vindue',
'binary_sensor.lille_bad_vindue',
'binary_sensor.terrassedor'
] %}
{% set ns = namespace(open=0) %}
{% for s in sensors %}{% if is_state(s, 'on') %}{% set ns.open = ns.open + 1 %}{% endif %}{% endfor %}
{{ 'Alle vinduer lukket' if ns.open == 0 else ns.open | string + ' vindue(r) åben' }}
secondary: >
{% set sensor_map = {
'binary_sensor.andreas_vindue': 'Andreas',
'binary_sensor.daniel_vindue': 'Daniel',
'binary_sensor.sovevaerelse_vindue': 'Soveværelse',
'binary_sensor.badevaerelse_vindue': 'Badeværelse',
'binary_sensor.lille_bad_vindue': 'Lille bad',
'binary_sensor.terrassedor': 'Terrassedør'
} %}
{% set ns = namespace(aabne=[]) %}
{% for s, n in sensor_map.items() %}{% if is_state(s, 'on') %}{% set ns.aabne = ns.aabne + [n] %}{% endif %}{% endfor %}
{{ ns.aabne | join(', ') if ns.aabne else '' }}
icon: >
{% set sensors = [
'binary_sensor.andreas_vindue',
'binary_sensor.daniel_vindue',
'binary_sensor.sovevaerelse_vindue',
'binary_sensor.badevaerelse_vindue',
'binary_sensor.lille_bad_vindue',
'binary_sensor.terrassedor'
] %}
{% set ns = namespace(open=0) %}
{% for s in sensors %}{% if is_state(s, 'on') %}{% set ns.open = ns.open + 1 %}{% endif %}{% endfor %}
{{ 'mdi:window-open-variant' if ns.open > 0 else 'mdi:window-closed-variant' }}
icon_color: >
{% set sensors = [
'binary_sensor.andreas_vindue',
'binary_sensor.daniel_vindue',
'binary_sensor.sovevaerelse_vindue',
'binary_sensor.badevaerelse_vindue',
'binary_sensor.lille_bad_vindue',
'binary_sensor.terrassedor'
] %}
{% set ns = namespace(open=0) %}
{% for s in sensors %}{% if is_state(s, 'on') %}{% set ns.open = ns.open + 1 %}{% endif %}{% endfor %}
{{ 'red' if ns.open > 0 else 'green' }}
tap_action:
action: none
# 🚗 Garage
- type: custom:mushroom-template-card
entity: binary_sensor.garageport
primary: >
{{ 'Garage åben' if is_state('binary_sensor.garageport', 'on') else 'Garage lukket' }}
secondary: >
Sidst ændret: {{ relative_time(states.binary_sensor.garageport.last_changed) }} siden
icon: >
{{ 'mdi:garage-open-variant' if is_state('binary_sensor.garageport', 'on') else 'mdi:garage-variant' }}
icon_color: >
{{ 'orange' if is_state('binary_sensor.garageport', 'on') else 'green' }}
tap_action:
action: call-service
service: cover.toggle
target:
entity_id: cover.anne
# 🏖️ Ferietilstand
- type: custom:mushroom-template-card
entity: input_boolean.vacation_mode
primary: >
{{ 'Ferie aktiv' if is_state('input_boolean.vacation_mode', 'on') else 'Normal tilstand' }}
secondary: >
{% if is_state('input_boolean.vacation_mode', 'on') %}
{% set end = states('input_datetime.vacation_end') %}
{% if end not in ['unknown', 'unavailable', ''] %}Slutter {{ as_datetime(end).strftime('%-d. %b') }}{% endif %}
{% endif %}
icon: >
{{ 'mdi:beach' if is_state('input_boolean.vacation_mode', 'on') else 'mdi:home' }}
icon_color: >
{{ 'cyan' if is_state('input_boolean.vacation_mode', 'on') else 'grey' }}
tap_action:
action: more-info
# 🤖 AI-overvågning (indkørsel)
- type: custom:mushroom-template-card
primary: >
{% set pause = states('input_datetime.ai_indkorsel_ai_pause_until') %}
{% set paused = pause not in ['unknown', 'unavailable', ''] and as_timestamp(pause) > as_timestamp(now()) %}
{{ 'AI-overvågning pauset' if paused else 'AI-overvågning aktiv' }}
secondary: >
{% set pause = states('input_datetime.ai_indkorsel_ai_pause_until') %}
{% set paused = pause not in ['unknown', 'unavailable', ''] and as_timestamp(pause) > as_timestamp(now()) %}
{% if paused %}Genoptages kl. {{ as_datetime(pause).strftime('%H:%M') }}{% else %}Indkørsel overvåges{% endif %}
icon: >
{% set pause = states('input_datetime.ai_indkorsel_ai_pause_until') %}
{% set paused = pause not in ['unknown', 'unavailable', ''] and as_timestamp(pause) > as_timestamp(now()) %}
{{ 'mdi:robot-off' if paused else 'mdi:robot' }}
icon_color: >
{% set pause = states('input_datetime.ai_indkorsel_ai_pause_until') %}
{% set paused = pause not in ['unknown', 'unavailable', ''] and as_timestamp(pause) > as_timestamp(now()) %}
{{ 'orange' if paused else 'green' }}
tap_action:
action: more-info
entity: input_datetime.ai_indkorsel_ai_pause_until
# 🔒 Terrassedør (separat overblik)
- type: custom:mushroom-template-card
entity: binary_sensor.terrassedor
primary: >
{{ 'Terrassedør åben' if is_state('binary_sensor.terrassedor', 'on') else 'Terrassedør lukket' }}
secondary: >
Sidst ændret: {{ relative_time(states.binary_sensor.terrassedor.last_changed) }} siden
icon: >
{{ 'mdi:door-open' if is_state('binary_sensor.terrassedor', 'on') else 'mdi:door-closed' }}
icon_color: >
{{ 'red' if is_state('binary_sensor.terrassedor', 'on') else 'green' }}
tap_action:
action: more-info
# 📡 Bevægelse i indkørslen lige nu
- type: custom:mushroom-template-card
entity: binary_sensor.indkorsel_sensor_motion
primary: >
{{ 'Bevægelse registreret!' if is_state('binary_sensor.indkorsel_sensor_motion', 'on') else 'Ingen bevægelse' }}
secondary: >
Sidst: {{ relative_time(states.binary_sensor.indkorsel_sensor_motion.last_changed) }} siden
icon: >
{{ 'mdi:motion-sensor' if is_state('binary_sensor.indkorsel_sensor_motion', 'on') else 'mdi:motion-sensor-off' }}
icon_color: >
{{ 'red' if is_state('binary_sensor.indkorsel_sensor_motion', 'on') else 'grey' }}
tap_action:
action: none
# ─────────────────────────────────────────────────────────────
# 📋 SENESTE BEVÆGELSE INDKØRSEL (AI-log)
# ─────────────────────────────────────────────────────────────
- type: heading
heading: Seneste bevægelse Indkørsel
heading_style: title
# Seneste snapshot gemt af AI-overvågningsscriptet
- type: picture-entity
entity: camera.indkorsel_snapshot
show_name: false
show_state: false
tap_action:
action: fire-dom-event
browser_mod:
service: browser_mod.popup
data:
title: Seneste bevægelse Indkørsel
content:
type: picture-entity
entity: camera.indkorsel_snapshot
show_name: false
show_state: false
tap_action:
action: none
# Seneste AI-beskrivelse
- type: custom:button-card
entity: input_text.last_notification_message
show_name: false
show_icon: true
show_state: true
icon: mdi:robot
styles:
card:
- padding: 14px 16px
- text-align: left
grid:
- grid-template-areas: '"i s"'
- grid-template-columns: 44px 1fr
- grid-template-rows: auto
icon:
- width: 32px
- height: 32px
- color: var(--primary-color)
- align-self: flex-start
- margin-top: 2px
state:
- white-space: normal
- word-break: break-word
- font-size: 13px
- text-align: left
- line-height: "1.5"
tap_action:
action: none
# ─────────────────────────────────────────────────────────────
# 📸 SENESTE PERSON-SNAPSHOT INDKØRSEL
# ─────────────────────────────────────────────────────────────
- type: heading
heading: Seneste person i indkørsel
heading_style: subtitle
# Klik åbner galleri med alle tidligere snapshots
- type: picture
image: /local/snapshots/indkorsel/latest.jpg
tap_action:
action: navigate
navigation_path: /lovelace/indkorsel-snapshots
# Logbog over bevægelseshændelser (48 timer)
- type: logbook
entities:
- binary_sensor.indkorsel_sensor_motion
hours_to_show: 48
title: Bevægelseslog (48 timer)
@@ -0,0 +1,9 @@
title: Snapshots Indkørsel
path: indkorsel-snapshots
icon: mdi:camera-burst
panel: true
cards:
- type: iframe
url: /local/snapshots/indkorsel_loader.html?v=20260516103651
aspect_ratio: 100%
+13
View File
@@ -0,0 +1,13 @@
services:
home-assistant:
container_name: homeassistant
image: homeassistant/home-assistant:latest
volumes:
- /volume1/homeassistant:/config
- /volume1/docker/homeassistant/backups:/backups
devices:
- /dev/ttyUSB0:/dev/ttyUSB0
network_mode: host
restart: always
environment:
- TZ=Europe/Copenhagen
+20
View File
@@ -37,6 +37,26 @@ services:
retries: 10 retries: 10
start_period: 20s start_period: 20s
nginx-proxy-manager:
container_name: nginx-proxy-manager
image: jc21/nginx-proxy-manager:latest
restart: unless-stopped
ports:
- "10080:80" # HTTP (inkl. Let's Encrypt HTTP-01 challenge) router forwarder 80→10080
- "10443:443" # HTTPS router forwarder 443→10443
- "10.0.0.142:81:81" # Admin UI kun tilgængeligt fra LAN
volumes:
- ${DOCKER_ROOT:-/volume1/docker}/nginx-proxy-manager/data:/data
- ${DOCKER_ROOT:-/volume1/docker}/nginx-proxy-manager/letsencrypt:/etc/letsencrypt
extra_hosts:
- "host.docker.internal:host-gateway" # Gør det muligt at proxye til HA på host-netværket
healthcheck:
test: ["CMD", "/usr/bin/check-health"]
interval: 10s
timeout: 3s
retries: 3
start_period: 20s
gitea: gitea:
container_name: gitea container_name: gitea
image: gitea/gitea:${GITEA_IMAGE_TAG:-latest} image: gitea/gitea:${GITEA_IMAGE_TAG:-latest}
+45 -27
View File
@@ -1,30 +1,48 @@
# TODO - Pending Tasks # TODO - Pending Tasks
## Home Assistant - Åbne opgaver
### HA-fejl der skal fikses
- [x] **Mealie shopping merge timeout** — udgået, merge med Google Keep droppes. Shopping-liste køres direkte i Mealie hver onsdag morgen.
- [x] **aiohttp 400 Bad Request fra ekstern IP** — Løst: Nginx Proxy Manager sat op som HTTPS reverse proxy (`anneclaus.duckdns.org`). Let's Encrypt cert udstedt, Force HTTPS aktiveret. Port 8123 lukket på routeren. HA tilgås nu udelukkende via HTTPS på port 443.
- [x] **switch.home_charging mangler** — Bilen oplades fint (16. maj 2026). Ikke et reelt problem.
- [x] **climate.badevarelse** — Danfoss Ally TRV monteret og online (7. maj 2026).
- [x] **Husqvarna Automower BLE — genopsæt parring** — Kørte fint. Problemet var at telefon-app'en kørte i baggrunden og holdt BLE-forbindelsen, selvom klipperen var slettet fra appen.
- [x] **Google AI MAX_TOKENS i AI-indkørsel automation** — Ingen fejl observeret i loggen. Fjernet fra aktiv liste.
### HA - Kendte ikke-fejl (ingen handling nødvendig)
- `husqvarna_automower_ble` BLE fejl — normalt når plæneklipperen klipper (men se auth fail-fejl ovenfor)
- `light.spejl1/spejl2` mangler — strøm slukket på kontakt, OK
## Sikring af port forwards via NPM + subdomæner
**Baggrund:** Diverse tjenester er direkte eksponeret via port-forwards på routeren uden kryptering. Disse bør flyttes bag NPM med HTTPS og subdomæner under `anneclaus.dk`.
**Tjenester fra routeren (per 17. maj 2026):**
| Tjeneste | WAN-port | Intern port | Forslag til subdomain |
|---|---|---|---|
| Mealie | 9925 | 9925 | `mealie.anneclaus.dk` |
| Plex | 32400 | 32400 | `plex.anneclaus.dk` (Plex har egen HTTPS — lavere prioritet) |
| Synology DSM | 5000/5001 | 5000/5001 | `nas.anneclaus.dk` |
| Unifi | 8443 | 8443 | (Unifi har egen HTTPS — lavere prioritet) |
| FTP | 21 | 21 | FTP er usikkert — overvej om den skal lukkes |
| MQTT | 1883 | 1883 | MQTT i klartekst — høj risiko |
| SSH | 2222 | 2222 | SSH er krypteret, men tiltrækker brute-force |
**For hver tjeneste der flyttes:**
1. Tilføj CNAME-record hos One.com: `<subdomain>``anneclaus.duckdns.org`
2. Opret proxy host i NPM med SSL-cert og Force HTTPS
3. Luk den direkte port-forward på routeren
- [x] **MQTT port 1883** — lukket på routeren (17. maj 2026). MQTT bruges kun internt af Aqara-knapper til ringklokke — ingen ekstern adgang nødvendig.
- [x] **FTP port 21** — lukket på routeren (17. maj 2026).
- [x] **Mealie**`mealie.anneclaus.dk` via NPM med HTTPS (18. maj 2026). Port 9925 lukket på routeren.
- [x] **Synology DSM**`nas.anneclaus.dk` via NPM med HTTPS (19. maj 2026). Port 5000/5001 lukket på routeren.
- [ ] **SSH port 2222** — overvej at begrænse til nøgle-login og deaktivere password-login
- [~] **Plex** — springer over. Plex krypterer selv sin trafik, og alle klientenheder (telefoner, iPad, Apple TV) skulle opdateres manuelt. Ikke umagen værd.
- [ ] **Unifi** — lavere prioritet, Unifi har allerede HTTPS
---
## Gitea External HTTPS Access ## Gitea External HTTPS Access
**Status:** Partial - content accessible but SSL certificate warning - [x] **gitea.anneclaus.dk via NPM** — CNAME oprettet hos One.com, NPM proxy host med Let's Encrypt + Force SSL, `.env.infrastructure` opdateret, container genstartet. HTTPS 200 OK (20. maj 2026).
### Task 1: SSL Certificate Binding
- [ ] Access Synology DSM Control Panel
- [ ] Navigate to Security → Certificate
- [ ] Bind the SSL certificate to `gitea.anneclaus.synology.me` in the reverse proxy configuration
- [ ] If certificate missing: Obtain new Let's Encrypt certificate for the hostname
- [ ] Test: Verify no SSL warning when accessing https://gitea.anneclaus.synology.me/
### Task 2: Update Gitea Configuration for External URL
- [ ] Edit `.env.infrastructure` file
- [ ] Update the following variables:
```
GITEA_DOMAIN=gitea.anneclaus.synology.me
GITEA_ROOT_URL=https://gitea.anneclaus.synology.me/
GITEA_SSH_DOMAIN=gitea.anneclaus.synology.me
```
- [ ] Restart Gitea container:
```bash
docker compose --env-file .env.infrastructure -f docker-compose.infrastructure.yml up -d gitea
```
- [ ] Test: Verify Git clone links show external URL
### Notes
- DNS routing and reverse proxy already working (content is accessible)
- Only certificate binding and Gitea configuration update remaining
- These changes will enable full external functionality for Git operations
+85
View File
@@ -0,0 +1,85 @@
# Procedure for fuld netværksgenstart
Brug denne procedure ved fejlfinding, strømafbrydelse eller planlagt vedligehold.
Formål: sikre at alle enheder starter i korrekt rækkefølge og at Home Assistant
kan forbinde til alle integrationer ved opstart.
---
## NEDLUKNING
Luk i denne rækkefølge — afhængige enheder lukkes **før** infrastruktur.
1. Luk NAS (Synology) ned via DSM eller SSH
2. Sluk Sonos-enheder i alle rum og tag stikket ud
3. Sluk PlayStation 5
4. Sluk printer og Sonos S1 på kontakten
5. Luk Mac mini og gamer-PC ned
6. Sluk Ubiquiti Access Points (kontor og stue)
7. Sluk Roth Touchline controller
8. Sluk gardin-controller (Hunter Douglas hub)
9. Sluk Denon forstærker
10. Sluk Google Nest Mini
11. Sluk Netatmo central enhed
12. Sluk Hue bridge
13. Sluk Ubiquiti switch
14. Sluk router
15. Sluk bredbåndsmodem (fiber)
---
## OPSTART
Start i denne rækkefølge — infrastruktur **før** afhængige enheder.
### Netværk (fundament)
1. **Bredbåndsmodem** — vent **5 min** til synkronisering
2. **Router** — vent **5 min**
3. **Ubiquiti switch** — vent **2 min**
4. **Ubiquiti Access Points** (stue og kontor) — vent **2 min**
### Enheder HA afhænger af ved opstart — tænd ALLE før NAS
5. **Hue bridge**
6. **Roth Touchline controller**
7. **Gardin-controller** (Hunter Douglas hub)
8. **Netatmo central enhed**
9. **Sonos Port** (central Sonos-enhed i stuen) — vent til den er online
10. **Sonos-enheder** — tænd én ad gangen, startende tættest på Sonos Port
11. **Denon receiver**
12. **Google Nest Mini** *(smart speaker/Google Assistant)*
13. **Printer og Sonos S1**
### NAS og Home Assistant
14. **NAS (Synology)** — tænd og vent **10 min**
- Home Assistant Docker-container starter automatisk
- HA bruger 35 min på at initialisere alle integrationer
- Tjek at HA er oppe: åbn `http://homeassistant.local:8123`
### Verificer Home Assistant
15. Tjek HA-loggen for fejl:
- Gå til **Indstillinger → System → Log** i HA
- Forventede (acceptable) fejl ved opstart:
- `husqvarna_automower_ble` — plæneklipper ikke i paringstilstand (normalt)
- `cover.terrasse_dor` / `cover.hojre` — Hunter Douglas timing (forsvinder efter et par min)
- `light.spejl1`, `light.spejl2` — kun tilgængelige når manuel kontakt er tændt
- Fejl der **kræver handling**:
- `Can not write request body for https://10.0.0.154` → Hue bridge reagerer ikke, genstartden igen
- `touchline` timeout hvert minut → Touchline CGI API stadig nede, prøv genstart af Touchline
### Øvrige enheder (ikke styret af HA)
16. **Mac mini, PS5, gamer-PC**
---
## KENDTE PROBLEMER OG STATUS (maj 2026)
| Integration | Problem | Løsning |
|---|---|---|
| Roth Touchline | CGI API (`/cgi-bin/ILRReadValues.cgi`) returnerer HTTP 000 | Netværksgenstart hjælper typisk |
| Hue bridge (10.0.0.154) | "Can not write request body" ved RAM-udtømning | HA-restart eller Hue-genstart |
| Zigbee USB | Periodisk `NcpFailure: ERROR_EXCEEDED_MAXIMUM_ACK_TIMEOUT_COUNT` | Acceptabelt, genoprettes automatisk |
| RAM | 3.7 GiB total — RAM-opgradering bestilt | Installer ny RAM når den ankommer |
---
*Opdateret: maj 2026*
+73 -10
View File
@@ -1,6 +1,25 @@
# Ønskeliste Nyt udstyr til Home Assistant # Ønskeliste Nyt udstyr til Home Assistant
*Sidst opdateret: april 2026* *Sidst opdateret: maj 2026*
---
## Netværk / UniFi
### IoT VLAN-segmentering (trin 4 fra UniFi check 10. maj 2026)
**Formål:** Isolere IoT-enheder fra primært hjemmenetværk af sikkerhedshensyn.
**Plan:**
- Nyt netværk: `IoT` med VLAN 20, subnet `192.168.20.0/24`
- Genaktivér eksisterende `sonoff`-SSID og tilknyt til IoT-netværk
- Firewall-regler:
- IoT → LAN: **BLOKERET**
- Home Assistant → IoT: **TILLADT** (nødvendigt for styring)
- IoT → Internet: **TILLADT**
- Enheder der skal flyttes til IoT-netværk: Sonoff, Zaptec, Tesla-lader, Wavin, øvrige IoT
**Kræver:** Manuel re-tilslutning af WiFi-enheder til nyt SSID. Kablede enheder tildeles VLAN via switch-port.
--- ---
@@ -10,7 +29,7 @@
| Antal | Rum | Beskrivelse | Status | | Antal | Rum | Beskrivelse | Status |
|---|---|---|---| |---|---|---|---|
| 1 | Badeværelse | Danfoss Ally TRV (Zigbee) | 🔧 To be fixed (kan ikke skrues ordentligt fast) | | 1 | Badeværelse | Danfoss Ally TRV (Zigbee) | ✅ Monteret og online (7. maj 2026) |
**Bekræftet ventiltype:** Danfoss RA (snap-on clips) Ally passer direkte med medfølgende RA-adapter. **Bekræftet ventiltype:** Danfoss RA (snap-on clips) Ally passer direkte med medfølgende RA-adapter.
@@ -46,22 +65,42 @@
--- ---
### Gulvvarme: Wavin bryggers + køkken Sonoff ZBMINI-L2 + temp-sensorer
| Antal | Enhed | Beskrivelse | Status |
|---|---|---|---|
| 2 | Sonoff ZBMINI-L2 | Zigbee relæ, erstatter Wavin RF-modtager | ⬜ Ønsket |
| 2 | SONOFF SNZB-02D | Zigbee temperatur/fugt sensor | ⬜ Ønsket |
**Baggrund:** Bryggers og køkken har i dag en dumb Wavin RF-modtager (JT6/3003-boksen) med to relækanaler (X = bryggers, Y = køkken) der styres af simple trådløse Wavin-termostater. Ingen smart protokol.
**Plan:** ZBMINI-L2 sættes i serie med fasen ud til aktuatoren (kanalernes brune ledning ud) inde i Wavin-boksen. Temp-sensor per rum. HA `generic_thermostat` samler dem til climate-entiteter der integreres med `script.varme_recalculate` som de øvrige rum.
**Se:** `dokumenter/wavin_sonoff_installation.md` for komplet installationsguide.
**Pris:** ~260 kr (ZBMINI) + ~200 kr (temp-sensorer) = **~460 kr total**
---
## Normal prioritet ## Normal prioritet
### Vindues-/dørsensorer Aqara ### Vindues-/dørsensorer Aqara
| Antal | Placering | Beskrivelse | Status | | Antal | Placering | Beskrivelse | Status |
|---|---|---|---| |---|---|---|---|
| 2 | Køkken | Aqara kontaktsensor (vindue) | ⬜ Ønsket | | 4 | Køkken | Aqara kontaktsensor (vindue) | ⬜ Ønsket |
| 1 | Bryggers | Aqara kontaktsensor (vindue) | ⬜ Ønsket | | 2 | Bryggers | Aqara kontaktsensor (vindue) | ⬜ Ønsket |
| 2 | Forgang | Aqara kontaktsensor (vindue/dør) | ⬜ Ønsket | | 4 | Forgang | Aqara kontaktsensor (vindue/dør) | ⬜ Ønsket |
| 1 | Stue | Aqara kontaktsensor (vindue) | ⬜ Ønsket | | 2 | Stue | Aqara kontaktsensor (vindue) | ⬜ Ønsket |
| 1 | Fordør | Aqara kontaktsensor (dør) | ⬜ Ønsket |
| 1 | Bagdør | Aqara kontaktsensor (dør) | ⬜ Ønsket |
| 1 | Kontor | Aqara kontaktsensor (vindue) | ⬜ Ønsket |
**Total:** 6 stk. Aqara Door & Window Sensor (Zigbee) **Total:** 15 stk. Aqara Door & Window Sensor (Zigbee) — 6 eksisterende + 9 nye
**Formål:** Automatisk stop af gulvvarme i rum med åbent vindue — samme logik som eksisterende sensorer i Andreas, Daniel, Soveværelse og Lille bad. Kobles direkte ind i `script.varme_recalculate` med if-betingelse per rum. **Formål:** Automatisk stop af gulvvarme i rum med åbent vindue — samme logik som eksisterende sensorer i Andreas, Daniel, Soveværelse og Lille bad. Kobles direkte ind i `script.varme_recalculate` med if-betingelse per rum.
**Bemærk:** Forgang-sensorer dækker sandsynligvis yderdøren + en vinduesramme — afklar placering inden køb. **Bemærk:** Afklar præcis placering (vinduesramme vs. dørfals) inden køb, særligt forgang og fordør/bagdør.
--- ---
@@ -69,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. 150200 kr. hos Komplett/Dustin. **Resultat:** DS920+ kører nu med 4GB (loddet) + 16GB SODIMM = **20GB total RAM**. Bekræftet i DSM Info Center. Sundhedscheck 13. maj 2026: 3,4GB brugt af 19GB tilgængeligt, swap-brug 0B — ingen memory-pressure. Alle 8 Docker containere kører healthy.
--- ---
@@ -98,6 +137,30 @@
| Luftkvalitetssensor | VOC / PM2.5 | Udvidelse af eksisterende CO₂-måling | | Luftkvalitetssensor | VOC / PM2.5 | Udvidelse af eksisterende CO₂-måling |
| Energimåler (CT-clamp) | Realtids strømmåling pr. kredsløb | Supplement til Eloverblik | | Energimåler (CT-clamp) | Realtids strømmåling pr. kredsløb | Supplement til Eloverblik |
---
## Vildtkamera
**Krav:**
- Batteri-drevet (ingen strøm på placeringen)
- WiFi-upload — ingen SD-kortafhentning
- Integration med Home Assistant og/eller Synology NAS
**Valgt: Reolink Argus 4 Pro + Reolink solpanel**
- Batteri + solpanel → aldrig behov for genopladning
- WiFi 6 — upload direkte til NAS/cloud uden SD-kort
- Officiel HA-integration (bevægelses-`binary_sensor` + `camera`-entitet med live stream og snapshot)
- Virker med Synology Surveillance Station via RTSP
- Farve-natvisning (spotlight)
| Enhed | Antal | Status |
|---|---|---|
| Reolink Argus 4 Pro | 1 | ⬜ Ønsket |
| Reolink solpanel (kompatibelt) | 1 | ⬜ Ønsket |
--- ---
## Indkøbt ✅ ## Indkøbt ✅
+252
View File
@@ -0,0 +1,252 @@
# Gulvvarme: Wavin bryggers + køkken → HA styring
## Idiot-sikker installationsguide
**Formål:** Erstatte den dumme RF-modtager (Wavin JT6/3003-boksen) med to Sonoff ZBMINI Zigbee-relæer,
så Home Assistant kan styre bryggers og køkken-gulvvarme præcis som de andre rum.
---
## Del 1: Indkøb
| Vare | Antal | Pris ca. | Link/søg |
|------|-------|----------|----------|
| **Sonoff ZBMINI-L2** (Zigbee relæ, ingen nul-ledning) | 2 | ~130 kr/stk | Aliexpress, Elgiganten |
| **SONOFF SNZB-02D** Zigbee temp/fugt sensor | 2 | ~100 kr/stk | Aliexpress |
> **Vigtigt:** Vælg ZBMINI-**L2** (eller ZBMINI Extreme) den kræver **ikke** en nuleder (N).
> Wavin-boksen har måske ikke nuleder fremme til brug for et relæ.
---
## Del 2: Forståelse af Wavin-boksen
Når du kigger på det grønne printplade med låget af:
```
MAINS IND (fra stikkontakt i væggen):
Brun = FASE (L) "det farlige"
Blå = NUL (N)
KANAL X (til aktuator 1, fx bryggers):
Brun = FASE UD til aktuator
KANAL Y (til aktuator 2, fx køkken):
Brun = FASE UD til aktuator
Aktuatorerne får NUL fra boksen via blå ledning.
```
Boksen virker som et simpelt on/off relæ per kanal:
- Når termostaten sender "varm op" → relæet lukker → 230V fase sendes ud til aktuatoren → ventil åbner
- Sonoff ZBMINI erstatter præcis dette relæ
---
## Del 3: Installation trin for trin
### ⚠️ STOP Sluk strøm FØR du rører noget
1. Find den sikring eller kontakt der forsyner Wavin-boksen
2. Sluk den
3. Brug en spændingsprøver/-tester på de brune ledninger inde i boksen bekræft at der er 0V
---
### Trin 1: Fotografér ledningerne i boksen FØR du piller noget
Tag et billede med din telefon. Du vil gerne huske hvad der sidder hvor.
---
### Trin 2: Identificér de 4 relevante ledninger
I Wavin-boksen sidder:
- **Brun ind** = Fase fra væggen (fælles for begge kanaler)
- **Blå ind** = Nul fra væggen (fælles)
- **Brun ud X** = Fase ud til aktuator bryggers
- **Brun ud Y** = Fase ud til aktuator køkken
(De blå ledninger der går ud er nuleder direkte til aktuatorerne de ændres ikke)
---
### Trin 3: Monter Sonoff ZBMINI-L2 nr. 1 (bryggers)
ZBMINI-L2 har disse klemmer:
```
[ L in ] [ L out ] [ S1 ] [ S2 ]
```
Tilslut:
- **L in** ← Brun fase ind fra væggen (eller tag en aftapning fra eksisterende brun)
- **L out** → Brun fase ud til bryggers-aktuatoren (den ledning der tidligere sad i X-relæet)
- **S1/S2** = bruges kun hvis du vil have en fysisk kontakt lad dem sidde tomme
Sonoff ZBMINI-L2 kræver ikke N (nuleder) det er pointen med L2-modellen.
---
### Trin 4: Monter Sonoff ZBMINI-L2 nr. 2 (køkken)
Identisk som trin 3, men brug Y-kanalens udgang:
- **L in** ← Brun fase ind (kan sidde på samme aftapning som nr. 1)
- **L out** → Brun fase ud til køkken-aktuatoren
---
### Trin 5: Wavin RF-modtagerboksen
Den eksisterende boks kobles nu **forbi** dens relæer bruges ikke længere.
Du kan enten:
- Efterlade den hængende (ufarlig, bare strøm ind og tomme udgange)
- Klippe strømmen til den (tag brun og blå ind ud af klemmerne og tape enderne)
Den gamle Wavin termostat på væggen virker stadig men gør intet du kan efterlade den eller tage den ned.
---
### Trin 6: Gendan strøm og test
1. Sæt strøm til igen
2. Begge Sonoff-enheder bør lyse rødt (venter på pairing)
---
## Del 4: Zigbee-pairing i Home Assistant
1. Gå til **Indstillinger → Enheder → Zigbee2MQTT** (eller ZHA hvis du bruger det)
2. Klik **Tillad tilslutning / Permit join** (60 sekunder)
3. Hold knappen på Sonoff ZBMINI nede i 5 sekunder til LED blinker hurtigt
4. Enheden dukker op navngiv den `bryggers_relæ` og `kokken_relæ`
5. Gentag for temp-sensorerne (tryk lille knap på siden for at parre)
---
## Del 5: Home Assistant konfiguration
### 5a: generic_thermostat (climate entity)
Tilføj til `configuration.yaml` (eller en inkluderet fil):
```yaml
climate:
- platform: generic_thermostat
name: Bryggers
unique_id: generic_thermostat_bryggers
heater: switch.bryggers_relae # Sonoff enhedens switch entity
target_sensor: sensor.bryggers_temp_sensor_temperature
min_temp: 15
max_temp: 28
target_temp: 20
cold_tolerance: 0.3
hot_tolerance: 0.3
min_cycle_duration:
minutes: 5
ac_mode: false
- platform: generic_thermostat
name: Køkken
unique_id: generic_thermostat_kokken
heater: switch.kokken_relae
target_sensor: sensor.kokken_temp_sensor_temperature
min_temp: 15
max_temp: 28
target_temp: 20
cold_tolerance: 0.3
hot_tolerance: 0.3
min_cycle_duration:
minutes: 5
ac_mode: false
```
> Tilpas entity-navnene til hvad Zigbee2MQTT faktisk kalder dem efter pairing.
### 5b: input_number til komforttemperaturer
Tilføj til `include/input/number/varme.yaml`:
```yaml
varme_komfort_bryggers:
name: Komfort - Bryggers
min: 15
max: 28
step: 0.5
unit_of_measurement: "°C"
initial: 20
icon: mdi:thermometer
varme_komfort_kokken:
name: Komfort - Køkken
min: 15
max: 28
step: 0.5
unit_of_measurement: "°C"
initial: 20
icon: mdi:thermometer
```
### 5c: Tilføj til varme_recalculate scriptet
De to nye rum skal med i `include/scripts/varme_styring.yaml``varme_recalculate`
på samme måde som badeværelse og stue (Danfoss Ally-mønsteret):
```yaml
# ---- Bryggers generic_thermostat ----
- if:
- condition: template
value_template: "{{ true }}" # ingen vinduessensor endnu
then:
- service: climate.set_temperature
target:
entity_id: climate.bryggers
data:
hvac_mode: heat
temperature: >
{% set k = states('input_number.varme_komfort_bryggers') | float(20) %}
{% if vacation %} {{ ferie_temp }}
{% elif night %} {{ [k - nat_sænk, 15] | max }}
{% elif not home %} {{ [k - vaek_sænk, 15] | max }}
{% else %} {{ k }}
{% endif %}
# ---- Køkken generic_thermostat ----
- if:
- condition: template
value_template: "{{ true }}"
then:
- service: climate.set_temperature
target:
entity_id: climate.kokken
data:
hvac_mode: heat
temperature: >
{% set k = states('input_number.varme_komfort_kokken') | float(20) %}
{% if vacation %} {{ ferie_temp }}
{% elif night %} {{ [k - nat_sænk, 15] | max }}
{% elif not home %} {{ [k - vaek_sænk, 15] | max }}
{% else %} {{ k }}
{% endif %}
```
---
## Del 6: Verificering
Når alt er sat op:
1. Gå til **Udviklerværktøjer → Tjenester**
2. Kald `climate.set_temperature``climate.bryggers` med `temperature: 25`
3. Lyt efter at aktuatoren klikker (kan høres eller mærkes) inden for 1-2 minutter
4. Sæt tilbage til normal komforttemperatur
---
## Resumé: Hvad du køber
| | |
|---|---|
| 2× Sonoff ZBMINI-L2 | ~260 kr |
| 2× Sonoff SNZB-02D temp-sensor | ~200 kr |
| **Total** | **~460 kr** |
Ingen elektriker, ingen nye kabler til aktuatorerne, ingen cloud-afhængighed.
+91
View File
@@ -0,0 +1,91 @@
# Zigbee husplan og netværksnoter
## Fysisk layout
```
NORD/INDKØRSEL-SIDE
══════════════════════════════════════════════════════════════════
GARAGE
[stik_fryser] ←── fjern ende (mod nord)
[garageport sensor] [stik_indkørsel] ←── tæt ende (mod kontor/syd)
══════════════ GARAGEMUR (beton - dæmper signal markant) ══════════
[stik_bryggers] [køkken] [forgang] [stik_lillebad] [stik_kontor]
↑ indkørslen løber langs denne side (bryggers → køkken → forgang → lille bad → badeværelse → kontor)
══════════════════════════════════════════════════════════════════
[badeværelse] [stik_bad]
[stik_sonos_stue] [stik_quooker] [stue]
[stik_daniel] (Daniels værelse - nabo til Andreas og over for bad)
[stik_soveværelse] (nabo til Daniel og kontor)
[stik_andreas] (Andreas værelse)
[stik_alrum]
SYD/STUE-SIDE
KOORDINATOR: Sonoff ZBDongle-E sidder på loftet over stue/Andreas-siden
```
## Zigbee enheder
| Enhed | Type | Placering |
|-------|------|-----------|
| SONOFF ZBDongle-E | Coordinator | Loft over stue/Andreas |
| stik_indkørsel | Router (TS011F) | Garage, tæt ende mod kontor |
| stik_fryser | Router (TS011F) | Garage, fjern ende mod nord |
| stik_kontor | Router (TS011F) | Kontor |
| stik_bryggers | Router (TS011F) | Bryggers |
| stik_lillebad | Router (TS011F) | Lille bad |
| stik_bad | Router (TS011F) | Badeværelse |
| stik_daniel | Router (TS011F) | Daniels værelse |
| stik_soveværelse | Router (TS011F) | Soveværelse |
| stik_andreas | Router (TS011F) | Andreas' værelse |
| stik_alrum | Router (TS011F) | Alrum |
| stik_sonos_stue | Router (TS011F) | Stue (Sonos) |
| stik_quooker | Router (TS011F) | Køkken (Quooker) |
| garageport | EndDevice (3RDTS01056Z) | Garage, tæt ende - tiltssensor på garageport |
| badevarelse | EndDevice | Badeværelse |
| stue | EndDevice | Stue |
| temp_bryggers | EndDevice | Bryggers (temperatursensor) |
| temp_køkken | EndDevice | Køkken (temperatursensor) |
| LUMI magnetsensorer (×6) | EndDevice | Spredt i huset |
## LQI-målinger over tid
| Enhed | 18/5 (morgen) | 18/5 (aften) | 20/5 | Bemærkning |
|-------|--------------|-------------|------|------------|
| stik_alrum | 184192 | 188192 | 184 | Stærk, tæt på koordinator |
| stik_andreas | 184192 | 188192 | 184 | Stærk, tæt på koordinator |
| stik_soveværelse | 76192 | 76188 | 84 | Svingende — route-afhængig |
| stik_quooker | 144152 | 124148 | 144 | God |
| stik_sonos_stue | 108152 | 108152 | 148 | God |
| stik_lillebad | 112136 | 112136 | 144 | OKGod |
| stik_daniel | — | 100120 | 92 | OK |
| stik_bryggers | 100132 | 100132 | 108 | OK |
| stik_bad | 40104 | 96144 | 144 | Forbedret |
| stik_kontor | 5296 | 7296 | 96 | Forbedret |
| stik_fryser | 5684 | 5684 | 84 | Forbedret |
| stik_indkørsel | 4480 | 56124 | 84 | OK efter genstart |
| garageport sensor | 4092 | 5292 | 84 | Bedste måling! |
| badevarelse | 108136 | 108136 | 144 | God |
| stue | 84116 | 84116 | 112 | OK |
| temp_bryggers | — | — | 148 | Ny 20/5 |
| temp_køkken | — | — | 152 | Ny 20/5 |
## Kendte problemer
- **Garagemuren** dæmper signalet markant — alle enheder bag muren har LQI 4080
- **Garageport-sensoren** er tæt på grænsen og har tidligere været unavailable
- Koordinatoren sidder i den modsatte ende af huset fra garagen
## Optimeringforslag
1. **Flyt koordinatoren til midten** (forgang/køkken-loftet) — størst effekt, kræver blot USB-forlænger
2. **Tilsæt router i forgang/gang** tæt på garagemur — bedre mellemled til garagen
3. stik_fryser er acceptabelt svag hvis fryseren blot er til strømmåling
## Entiteter der styrer indkørselslys
- `switch.stik_indkorsel` — Zigbee plug (erstattede `light.indkorsel_plug` fra Hue, maj 2026)
- `light.indkorsel_2` — Hue gruppe (garage venstre + højre + bryggersdør)
- `scene.indkorsel_bright` / `scene.indkorsel_dimmed` — Hue scener
- Automationer: `lysindkorsel.yaml`, `presence_simulation.yaml`, `andreas_kommer_hjem_taend_lys.yaml`
@@ -48,9 +48,17 @@
target: target:
entity_id: "{{ lights_to_turn_on }}" entity_id: "{{ lights_to_turn_on }}"
- service: switch.turn_on
data:
entity_id: switch.stik_indkorsel
- delay: "00:10:00" - delay: "00:10:00"
- service: light.turn_off - service: light.turn_off
target: target:
entity_id: "{{ lights_to_turn_on }}" entity_id: "{{ lights_to_turn_on }}"
- service: switch.turn_off
data:
entity_id: switch.stik_indkorsel
+10
View File
@@ -10,6 +10,8 @@
- condition: state # from sunset until sunrise - condition: state # from sunset until sunrise
entity_id: sun.sun entity_id: sun.sun
state: 'below_horizon' state: 'below_horizon'
- condition: template # Vintersæson uge 42-8
value_template: "{{ now().isocalendar()[1] >= 42 or now().isocalendar()[1] <= 8 }}"
action: action:
- service: light.turn_on - service: light.turn_on
data: data:
@@ -19,6 +21,9 @@
trigger: trigger:
platform: sun platform: sun
event: sunrise event: sunrise
condition:
- condition: template # Vintersæson uge 42-8
value_template: "{{ now().isocalendar()[1] >= 42 or now().isocalendar()[1] <= 8 }}"
action: action:
- service: light.turn_off - service: light.turn_off
data: data:
@@ -31,6 +36,8 @@
condition: condition:
- condition: time - condition: time
before: '21:30:00' before: '21:30:00'
- condition: template # Vintersæson uge 42-8
value_template: "{{ now().isocalendar()[1] >= 42 or now().isocalendar()[1] <= 8 }}"
action: action:
- service: light.turn_on - service: light.turn_on
data: data:
@@ -40,6 +47,9 @@
trigger: trigger:
platform: time platform: time
at: "22:00:00" at: "22:00:00"
condition:
- condition: template # Vintersæson uge 42-8
value_template: "{{ now().isocalendar()[1] >= 42 or now().isocalendar()[1] <= 8 }}"
action: action:
- service: light.turn_off - service: light.turn_off
data: data:
+24
View File
@@ -1,3 +1,27 @@
- id: badevaerelse_startup_sluk
alias: Badeværelse lys sluk ved HA opstart
description: >
Slukker badeværelsets lys ved genstart hvis bevægelsessensoren er inaktiv.
Sikrer mod lys der sidder tændt efter strømudfald eller HA-genstart.
mode: single
trigger:
- platform: homeassistant
event: start
action:
- delay:
seconds: 30
- condition: state
entity_id: binary_sensor.badevaerelse_bevaegelse
state: "off"
- service: light.turn_off
target:
area_id: badevaerelse
- service: input_boolean.turn_off
target:
entity_id: input_boolean.badevaerelse_manuel_tilstand
- id: badevaerelse_motion_lys - id: badevaerelse_motion_lys
alias: Badeværelse lys via bevægelse alias: Badeværelse lys via bevægelse
mode: restart mode: restart
+31 -5
View File
@@ -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,20 @@
- 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
- conditions:
- condition: template
value_template: "{{ dagperiode == 'aften' }}"
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 +102,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 +112,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 +123,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 }}"
+19 -1
View File
@@ -16,6 +16,9 @@
- service: scene.turn_on - service: scene.turn_on
data: data:
entity_id: scene.indkorsel_bright entity_id: scene.indkorsel_bright
- service: switch.turn_on
data:
entity_id: switch.stik_indkorsel
- alias: 'Sluk lys indkørsel når der er lys nok' - alias: 'Sluk lys indkørsel når der er lys nok'
trigger: trigger:
@@ -30,6 +33,9 @@
- service: light.turn_off - service: light.turn_off
data: data:
entity_id: light.indkorsel_2 entity_id: light.indkorsel_2
- service: switch.turn_off
data:
entity_id: switch.stik_indkorsel
- alias: 'Tænd lys indkørsel aften' - alias: 'Tænd lys indkørsel aften'
trigger: trigger:
@@ -43,6 +49,9 @@
- service: scene.turn_on - service: scene.turn_on
data: data:
entity_id: scene.indkorsel_bright entity_id: scene.indkorsel_bright
- service: switch.turn_on
data:
entity_id: switch.stik_indkorsel
- alias: 'Sluk lys indkørsel aften' - alias: 'Sluk lys indkørsel aften'
trigger: trigger:
@@ -52,6 +61,9 @@
- service: light.turn_off - service: light.turn_off
data: data:
entity_id: light.indkorsel_2 entity_id: light.indkorsel_2
- service: switch.turn_off
data:
entity_id: switch.stik_indkorsel
- alias: 'Tænd lys indkørsel ved bevægelse' - alias: 'Tænd lys indkørsel ved bevægelse'
@@ -78,6 +90,9 @@
- service: scene.turn_on - service: scene.turn_on
data: data:
entity_id: scene.indkorsel_bright entity_id: scene.indkorsel_bright
- service: switch.turn_on
data:
entity_id: switch.stik_indkorsel
- alias: 'Sluk lys indkørsel 15 min efter bevægelse' - alias: 'Sluk lys indkørsel 15 min efter bevægelse'
trigger: trigger:
@@ -97,8 +112,11 @@
# entity_id: sun.sun # entity_id: sun.sun
# state: below_horizon # state: below_horizon
action: action:
service: light.turn_off - service: light.turn_off
data: data:
entity_id: light.indkorsel_2 entity_id: light.indkorsel_2
- service: switch.turn_off
data:
entity_id: switch.stik_indkorsel
+11 -10
View File
@@ -1,13 +1,3 @@
- id: mealie_update_mealplan
alias: "Mealie opdater madplan"
trigger:
- platform: homeassistant
event: start
- platform: time_pattern
minutes: "/30"
action:
- service: shell_command.mealie_update
- id: mealie_generate_bilka_checklist_wednesday - 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
@@ -54,6 +54,10 @@
- delay: - delay:
seconds: "{{ range(30,180) | random }}" seconds: "{{ range(30,180) | random }}"
- service: switch.turn_on
data:
entity_id: switch.stik_indkorsel
- delay: - delay:
minutes: "{{ range(10,30) | random }}" minutes: "{{ range(10,30) | random }}"
@@ -66,6 +70,10 @@
- delay: - delay:
seconds: "{{ range(20,120) | random }}" seconds: "{{ range(20,120) | random }}"
- service: switch.turn_off
data:
entity_id: switch.stik_indkorsel
- choose: - choose:
- conditions: - conditions:
- condition: template - condition: template
@@ -146,6 +154,10 @@
- delay: - delay:
seconds: "{{ range(20,120) | random }}" seconds: "{{ range(20,120) | random }}"
- service: switch.turn_on
data:
entity_id: switch.stik_indkorsel
- delay: - delay:
minutes: "{{ range(15,60) | random }}" minutes: "{{ range(15,60) | random }}"
@@ -158,6 +170,10 @@
- delay: - delay:
seconds: "{{ range(20,120) | random }}" seconds: "{{ range(20,120) | random }}"
- service: switch.turn_off
data:
entity_id: switch.stik_indkorsel
- choose: - choose:
- conditions: - conditions:
- condition: template - condition: template
+7 -10
View File
@@ -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,39 @@
- alias: 'Indkorsel: Slet gamle snapshots (behold 100)'
description: Køres via webhook fra galleriet sletter alle undtagen de 100 nyeste snapshots og regenererer galleriet.
trigger:
- platform: webhook
webhook_id: indkorsel_prune_100
allowed_methods: [POST]
local_only: true
action:
- action: shell_command.indkorsel_prune_keep_100
- delay: '00:00:02'
- action: shell_command.indkorsel_generate_gallery
mode: single
- alias: 'Snapshot ved person i indkorsel'
description: >
Gemmer et tidsstemplet snapshot + opdaterer latest.jpg + regenererer HTML-galleri,
hver gang binary_sensor.indkoersel_person skifter til 'on'.
trigger:
- platform: state
entity_id: binary_sensor.indkoersel_person
to: 'on'
condition: []
action:
- variables:
ts: "{{ now().strftime('%Y-%m-%d_%H-%M-%S') }}"
# Gem tidsstemplet kopi
- action: camera.snapshot
data:
entity_id: camera.indkoersel_sub
filename: "/config/www/snapshots/indkorsel/{{ ts }}.jpg"
# Overskriv latest.jpg (bruges af local_file-kamera i dashboardet)
- action: camera.snapshot
data:
entity_id: camera.indkoersel_sub
filename: "/config/www/snapshots/indkorsel/latest.jpg"
# Regenerer HTML-galleriet
- action: shell_command.indkorsel_generate_gallery
mode: queued
max: 5
+1 -1
View File
@@ -9,7 +9,7 @@
id: varme_vindue_trigger 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:
+3
View File
@@ -0,0 +1,3 @@
gaester:
name: "Gæster hjemme"
icon: mdi:account-group
+1 -1
View File
@@ -4,7 +4,7 @@ shelly_bagdor_event_cnt:
max: 99999 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
+7 -7
View File
@@ -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:
@@ -50,7 +50,7 @@ varme_komfort_forgang:
max: 28 max: 28
step: 0.5 step: 0.5
unit_of_measurement: "°C" unit_of_measurement: "°C"
initial: 22 initial: 24
icon: mdi:thermometer icon: mdi:thermometer
varme_komfort_lille_bad: varme_komfort_lille_bad:
@@ -59,7 +59,7 @@ varme_komfort_lille_bad:
max: 28 max: 28
step: 0.5 step: 0.5
unit_of_measurement: "°C" unit_of_measurement: "°C"
initial: 22 initial: 24
icon: mdi:thermometer icon: mdi:thermometer
varme_komfort_badevarelse: varme_komfort_badevarelse:
@@ -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: 21.5
icon: mdi:thermometer icon: mdi:thermometer
varme_komfort_stue: varme_komfort_stue:
@@ -77,7 +77,7 @@ varme_komfort_stue:
max: 28 max: 28
step: 0.5 step: 0.5
unit_of_measurement: "°C" unit_of_measurement: "°C"
initial: 24 initial: 25
icon: mdi:thermometer icon: mdi:thermometer
# Globale sænkninger # Globale sænkninger
-2
View File
@@ -105,7 +105,6 @@
name: Indkørsel name: Indkørsel
unique_id: lys_indkorsel unique_id: lys_indkorsel
entities: entities:
- light.indkorsel_plug
- light.udendors_forgang - light.udendors_forgang
- light.hue_ambiance_lamp_1_2 - light.hue_ambiance_lamp_1_2
- light.hue_ambiance_lamp_1_3 - light.hue_ambiance_lamp_1_3
@@ -116,7 +115,6 @@
unique_id: lys_udenfor unique_id: lys_udenfor
entities: entities:
- light.garage - light.garage
- light.indkorsel_plug
- light.fordoer - light.fordoer
- light.julelys - light.julelys
+102
View File
@@ -1,3 +1,105 @@
vi_laver_mad:
alias: Vi laver mad
sequence:
- choose:
- conditions:
- condition: state
entity_id: person.andreas_schusler_dethlefsen
state: home
sequence:
- service: notify.mobile_app_andreas_iphone_12
data:
message: >-
{% set meal = states('sensor.dagens_aftensmad') %}
{% if meal and meal not in ['unknown','unavailable','Ingen planlagt'] %}
Vi laver mad! I dag: {{ meal }}
{% else %}
Vi laver mad!
{% endif %}
- choose:
- conditions:
- condition: state
entity_id: person.daniel_schusler_dethlefsen
state: home
sequence:
- service: notify.mobile_app_daniels_iphone_13_mini
data:
message: >-
{% set meal = states('sensor.dagens_aftensmad') %}
{% if meal and meal not in ['unknown','unavailable','Ingen planlagt'] %}
Vi laver mad! I dag: {{ meal }}
{% else %}
Vi laver mad!
{% endif %}
- choose:
- conditions:
- condition: state
entity_id: person.andreas_schusler_dethlefsen
state: home
sequence:
- service: sonos.snapshot
data:
entity_id: media_player.andreas
with_group: true
- service: media_player.media_stop
target:
entity_id: media_player.andreas
- service: media_player.volume_set
target:
entity_id: media_player.andreas
data:
volume_level: 0.35
- service: tts.speak
target:
entity_id: tts.google_ai_tts
data:
media_player_entity_id: media_player.andreas
message: >-
{% set meal = states('sensor.dagens_aftensmad') %}
{% if meal and meal not in ['unknown','unavailable','Ingen planlagt'] %}
Vi laver mad! I dag spiser vi {{ meal }}
{% else %}
Vi laver mad!
{% endif %}
- delay: "00:00:08"
- service: sonos.restore
data:
entity_id: media_player.andreas
- choose:
- conditions:
- condition: state
entity_id: person.daniel_schusler_dethlefsen
state: home
sequence:
- service: sonos.snapshot
data:
entity_id: media_player.daniel
with_group: true
- service: media_player.media_stop
target:
entity_id: media_player.daniel
- service: media_player.volume_set
target:
entity_id: media_player.daniel
data:
volume_level: 0.35
- service: tts.speak
target:
entity_id: tts.google_ai_tts
data:
media_player_entity_id: media_player.daniel
message: >-
{% set meal = states('sensor.dagens_aftensmad') %}
{% if meal and meal not in ['unknown','unavailable','Ingen planlagt'] %}
Vi laver mad! I dag spiser vi {{ meal }}
{% else %}
Vi laver mad!
{% endif %}
- delay: "00:00:08"
- service: sonos.restore
data:
entity_id: media_player.daniel
mad_announcement: mad_announcement:
alias: Der er mad alias: Der er mad
sequence: sequence:
+2 -2
View File
@@ -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."
+4
View File
@@ -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
+15
View File
@@ -223,3 +223,18 @@ varme_recalculate:
data: data:
hvac_mode: heat hvac_mode: heat
temperature: "{{ ferie_temp }}" temperature: "{{ ferie_temp }}"
varme_save_defaults:
alias: Gem varme-standardværdier
icon: mdi:content-save
sequence:
- action: shell_command.varme_save_defaults
response_variable: result
- action: persistent_notification.create
data:
title: Varme-standardværdier gemt
message: >
{{ result.stdout if result.returncode == 0
else 'Fejl: ' ~ result.stderr }}
notification_id: varme_save_defaults
+13
View File
@@ -0,0 +1,13 @@
##################################################
# Glidende gennemsnit af anbefalet fjernvarme-ventilposition
# Beregner mean over de seneste 21 dage, så anbefalingen
# bevæger sig sæsonmæssigt (uger) i stedet for dagligt.
##################################################
- platform: statistics
name: "Fjernvarme ventil 3 ugers gennemsnit"
entity_id: sensor.fjernvarme_ventil_anbefalet
state_characteristic: mean
sampling_size: 2000
max_age:
days: 21
+2
View File
@@ -0,0 +1,2 @@
indkorsel_generate_gallery: "/usr/local/bin/docker exec homeassistant python3 /config/python_scripts/generate_indkorsel_gallery.py"
indkorsel_prune_keep_100: "ls -1 /config/www/snapshots/indkorsel/*.jpg | grep -v '/latest.jpg$' | sort -r | tail -n +101 | xargs rm -f"
+1
View File
@@ -0,0 +1 @@
varme_save_defaults: "python3 /config/python_scripts/save_varme_defaults.py"
+18
View File
@@ -0,0 +1,18 @@
- trigger:
- platform: homeassistant
event: start
- platform: time_pattern
hours: "/1"
action:
- action: weather.get_forecasts
target:
entity_id: weather.norgardsvej
data:
type: daily
response_variable: daily
sensor:
- name: "Vejr daglig prognose"
unique_id: vejr_daglig_prognose
state: "{{ daily['weather.norgardsvej'].forecast | length }}"
attributes:
forecast: "{{ daily['weather.norgardsvej'].forecast }}"
+6
View File
@@ -45,3 +45,9 @@ Vigtige detaljer om min opsaetning:
- Telefoner: notify.mobile_app_claus_iphone_15pro, notify.mobile_app_annes_iphone_14_pro - Telefoner: notify.mobile_app_claus_iphone_15pro, notify.mobile_app_annes_iphone_14_pro
- Sonos hoejtalere: media_player.alrum, media_player.lille_badevaerelse, m.fl. - Sonos hoejtalere: media_player.alrum, media_player.lille_badevaerelse, m.fl.
- Git repo: gitea.anneclaus.synology.me (SSH) - Git repo: gitea.anneclaus.synology.me (SSH)
- Husqvarna Automower (lawn_mower.husqvarna_automower): BLE-baseret via husqvarna_automower_ble integration
- MAC: C4:64:E3:B1:16:14, BLE-proxy: ESP32 paa D0:CF:13:0D:01:16
- BLE tillader kun EN aktiv forbindelse ad gangen - Husqvarna-appen paa telefon SKAL lukkes helt
(ogsaa selvom moweren er slettet i appen - appen kan stadig holde BLE-session aaben i baggrunden)
- Fejlsymptom: "could not find device with address C4:64:E3:B1:16:14" i log, "mower returned 1" i UI
- Fix: Luk Husqvarna-appen helt paa alle enheder -> genstart integrationen i HA
@@ -0,0 +1,204 @@
#!/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 = 500
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>
.prune-btn{{display:inline-block;margin-left:12px;padding:3px 10px;font-size:11px;border:1px solid #c44;background:transparent;color:#c88;border-radius:12px;cursor:pointer;vertical-align:middle;transition:background .15s,color .15s}}
.prune-btn:hover{{background:#c44;color:#fff}}
.prune-btn:disabled{{opacity:.4;cursor:default}}
*{{box-sizing:border-box;margin:0;padding:0}}
body{{background:#111;color:#ddd;font-family:sans-serif;padding:10px}}
h2{{padding:8px 0 14px;font-size:13px;opacity:.5;font-weight:normal}}
.grid{{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:6px}}
.thumb{{cursor:pointer;border-radius:7px;overflow:hidden;background:#222;transition:transform .12s,opacity .12s}}
.thumb:hover{{transform:scale(1.02);opacity:.9}}
.thumb img{{width:100%;aspect-ratio:16/9;object-fit:cover;display:block}}
.ts{{font-size:10px;padding:4px 6px;opacity:.5;text-align:center}}
.modal{{display:none;position:fixed;inset:0;background:rgba(0,0,0,.93);z-index:9999;flex-direction:column;justify-content:center;align-items:center}}
.modal.show{{display:flex}}
.modal img{{max-width:96vw;max-height:82vh;border-radius:8px;object-fit:contain;box-shadow:0 4px 40px rgba(0,0,0,.6)}}
.modal-ts{{margin-top:12px;font-size:14px;opacity:.6;letter-spacing:.3px}}
.modal-counter{{margin-top:4px;font-size:11px;opacity:.35;letter-spacing:.3px}}
.close{{position:absolute;top:14px;right:18px;font-size:32px;cursor:pointer;opacity:.5;line-height:1;user-select:none}}
.close:hover{{opacity:1}}
.nav{{position:absolute;top:50%;transform:translateY(-50%);font-size:44px;cursor:pointer;opacity:.35;user-select:none;padding:0 18px;line-height:1}}
.nav:hover{{opacity:.9}}
.nav.prev{{left:0}} .nav.next{{right:0}}
.nav.disabled{{opacity:.08;cursor:default;pointer-events:none}}
.empty{{padding:40px;text-align:center;opacity:.4;font-size:14px}}
.reload-badge{{position:fixed;bottom:14px;right:14px;background:#1a73e8;color:#fff;padding:6px 14px;border-radius:20px;font-size:12px;cursor:pointer;display:none;z-index:9998}}
</style>
</head><body>
<h2>Viser {len(files)} person-snapshots &ndash; Indkorsel
<button class="prune-btn" id="pruneBtn" onclick="pruneSnapshots()" title="Slet alle undtagen de 100 nyeste">Behold sidste 100</button>
</h2>
{"<div class='grid'>" + items_html + "</div>" if files else "<div class='empty'>Ingen snapshots endnu.</div>"}
<div id="reload-badge" class="reload-badge" onclick="window.location.reload(true)">Nye billeder &#8211; tryk for at opdatere</div>
<div class="modal" id="modal">
<span class="close" onclick="closeModal()">&#x2715;</span>
<span class="nav prev" id="navPrev" onclick="navigate(-1)">&#x2039;</span>
<img id="mimg" src="" alt=""/>
<span class="nav next" id="navNext" onclick="navigate(1)">&#x203a;</span>
<div class="modal-ts" id="mts"></div>
<div class="modal-counter" id="mcounter"></div>
</div>
<script>
const IMGS = {images_js_str};
const VERSION = '{version}';
let cur = 0;
function openModal(idx){{
cur = idx;
render();
document.getElementById('modal').classList.add('show');
}}
function render(){{
const item = IMGS[cur];
document.getElementById('mimg').src = item.src;
document.getElementById('mts').textContent = item.ts;
document.getElementById('mcounter').textContent = (cur+1) + ' / ' + IMGS.length;
document.getElementById('navPrev').classList.toggle('disabled', cur === 0);
document.getElementById('navNext').classList.toggle('disabled', cur === IMGS.length - 1);
}}
function navigate(dir){{
const next = cur + dir;
if(next >= 0 && next < IMGS.length){{ cur = next; render(); }}
}}
function closeModal(){{
document.getElementById('modal').classList.remove('show');
document.getElementById('mimg').src = '';
}}
document.addEventListener('keydown', e => {{
if(!document.getElementById('modal').classList.contains('show')) return;
if(e.key === 'ArrowLeft') navigate(-1);
if(e.key === 'ArrowRight') navigate(1);
if(e.key === 'Escape') closeModal();
}});
document.getElementById('modal').addEventListener('click', function(e){{
if(e.target === this) closeModal();
}});
// Touch swipe support (iPhone/iPad)
let _tx = null;
document.getElementById('modal').addEventListener('touchstart', e => {{
_tx = e.changedTouches[0].clientX;
}}, {{passive: true}});
document.getElementById('modal').addEventListener('touchend', e => {{
if(_tx === null) return;
const dx = e.changedTouches[0].clientX - _tx;
_tx = null;
if(Math.abs(dx) < 40) return; // ignore taps
navigate(dx < 0 ? 1 : -1); // swipe left = næste, swipe right = forrige
}}, {{passive: true}});
function pruneSnapshots(){{
if(!confirm('Slet alle undtagen de 100 nyeste billeder?')) return;
const btn = document.getElementById('pruneBtn');
btn.disabled = true;
btn.textContent = 'Sletter...';
fetch('/api/webhook/indkorsel_prune_100', {{method:'POST'}})
.then(() => {{
btn.textContent = 'Færdig genindlæser...';
setTimeout(() => window.location.reload(true), 3500);
}})
.catch(() => {{
btn.disabled = false;
btn.textContent = 'Fejl prøv igen';
}});
}}
// 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}")
# Update dashboard YAML iframe URL version so browser always fetches fresh loader
DASHBOARD_VIEW = "/config/dashboards/views/06c_indkorsel_snapshots.yaml"
try:
import re as _re
with open(DASHBOARD_VIEW, "r", encoding="utf-8") as fh:
dash = fh.read()
updated = _re.sub(
r'(url: /local/snapshots/indkorsel_loader\.html)(\?v=\d+)?',
rf'\1?v={version}',
dash
)
if updated != dash:
with open(DASHBOARD_VIEW, "w", encoding="utf-8") as fh:
fh.write(updated)
except Exception as _e:
print(f"Warning: could not update dashboard YAML: {_e}")
+1 -5
View File
@@ -273,11 +273,7 @@ def main() -> None:
shopping_list_id = reset_shopping_list(MEALIE_BASE_URL, token, TARGET_SHOPPING_LIST_NAME, shopping_list_id) 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: '
+90
View File
@@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""
Gem nuværende varme-indstillinger som nye 'initial' standardværdier i YAML-filen.
Køres inde i homeassistant Docker-containeren.
"""
import re
import json
import urllib.request
YAML_FILE = "/config/include/input/number/varme.yaml"
SECRETS_FILE = "/config/secrets.yaml"
HA_URL = "http://localhost:8123"
ENTITIES = [
"varme_komfort_andreas",
"varme_komfort_daniel",
"varme_komfort_sovevaerelse",
"varme_komfort_kontor",
"varme_komfort_gang",
"varme_komfort_forgang",
"varme_komfort_lille_bad",
"varme_komfort_badevarelse",
"varme_komfort_stue",
"varme_nat_saenkning",
"varme_vaek_saenkning",
"varme_ferie_temp",
]
def get_token():
with open(SECRETS_FILE) as f:
for line in f:
if line.startswith("ha_token:"):
return line.split(":", 1)[1].strip()
raise ValueError("ha_token ikke fundet i secrets.yaml")
def get_states(token):
req = urllib.request.Request(
f"{HA_URL}/api/states",
headers={"Authorization": f"Bearer {token}"},
)
with urllib.request.urlopen(req) as resp:
return {d["entity_id"]: d["state"] for d in json.loads(resp.read())}
def format_value(state_str):
val = float(state_str)
return str(int(val)) if val == int(val) else str(val)
def update_initial(content, entity_name, new_value):
"""Erstat initial-værdien for en given entity i YAML-indholdet."""
pattern = rf"(^{re.escape(entity_name)}:\n(?: [^\n]*\n)*? initial: )\S+"
new_content, count = re.subn(
pattern, rf"\g<1>{new_value}", content, flags=re.MULTILINE, count=1
)
if count == 0:
print(f" ADVARSEL: {entity_name} ikke fundet i YAML")
return new_content
def main():
token = get_token()
states = get_states(token)
with open(YAML_FILE) as f:
content = f.read()
saved = []
for name in ENTITIES:
entity_id = f"input_number.{name}"
state = states.get(entity_id)
if state in (None, "unavailable", "unknown"):
print(f" SPRING OVER {entity_id}: {state}")
continue
val_str = format_value(state)
content = update_initial(content, name, val_str)
saved.append(f"{entity_id} = {val_str}")
with open(YAML_FILE, "w") as f:
f.write(content)
print(f"Gemt {len(saved)} standardværdier -> {YAML_FILE}")
for line in saved:
print(f" {line}")
if __name__ == "__main__":
main()
+5
View File
@@ -37,6 +37,11 @@ synology_password: boss3LEY8ogh!saub
## old nas was 10.0.0.189 ## old nas was 10.0.0.189
# ######################################## # ########################################
npm_url: http://10.0.0.142:81
npm_email: claus.dethlefsen@gmail.com
npm_password: Hwli03yitw
# ########################################
hue_ip: 10.0.0.154 hue_ip: 10.0.0.154
# ######################################## # ########################################
+49 -23
View File
@@ -18,57 +18,83 @@
</head> </head>
<body> <body>
<h1>🛒 Bilka ToGo</h1> <h1>🛒 Bilka ToGo</h1>
<p class="sub">Plan 01/05 07/05 &nbsp;·&nbsp; 45 varer</p> <p class="sub">Plan 15/05 21/05 &nbsp;·&nbsp; 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 dl hvidvin</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>0,50 tsk stødt spidskommen</td></tr> <tr><td class="cb"><input type="checkbox"></td><td>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 æ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 æggehvider</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 dl grøntsagsbouillon</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 mælk</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 hel knoldselleri (ca. 700-800 g)</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 spsk smør, til at smøre fadet</td></tr> <tr><td class="cb"><input type="checkbox"></td><td>1 knivspids røget paprika</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 knivspids sød paprika</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 spsk smør, til stegning</td></tr> <tr><td class="cb"><input type="checkbox"></td><td>1 spsk smør, til stegning</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 squash, groftrevet</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 tsk sød paprika</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 tsk tørret timian</td></tr> <tr><td class="cb"><input type="checkbox"></td><td>1 tsk tørret timian</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>100 g parmesan, fintrevet</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>2 fed hvidløg</td></tr> <tr><td class="cb"><input type="checkbox"></td><td>100 g stenbiderrogn</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2 spsk kapers (valgfrit)</td></tr> <tr><td class="cb"><input type="checkbox"></td><td>2 spsk kapers (valgfrit)</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>4 æggeblommer + 1 helt æg</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>4 gulerødder, skrællede og skiveskåret</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>400 g spaghetti eller rigatoni</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>50 g pecorino, fintrevet (kan erstattes af mere parmesan)</td></tr> <tr><td class="cb"><input type="checkbox"></td><td>3 dl mælk</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>4 grønne asparges</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>4 gulerødder, groftrevet</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>4 hvide asparges</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>4 skiver brød</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>5 stængler bladselleri, groftrevet</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>50 g rasp</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>75 g smør</td></tr> <tr><td class="cb"><input type="checkbox"></td><td>75 g smør</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>800 g kartofler, skrubbede</td></tr> <tr><td class="cb"><input type="checkbox"></td><td>8 rødspættefilet</td></tr>
<tr><th colspan="2" class="cat">Frost</th></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2 spsk mayonnaise</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2,50 dl piskefløde</td></tr>
<tr><th colspan="2" class="cat">Frugt & Grønt</th></tr> <tr><th colspan="2" class="cat">Frugt & Grønt</th></tr>
<tr><td class="cb"><input type="checkbox"></td><td>½ citron</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 fintrevet skal</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>Grøn salat</td></tr> <tr><td class="cb"><input type="checkbox"></td><td>1 citron, 1 spsk saft herfra</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 citron, skåret i både</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>12 skiver agurk</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2 banan, skåret i skiver på ca. 1 cm</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2 spsk koncentreret tomatpuré</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>4 håndfulde grøn salat, til anretning</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>50 g koncentreret tomatpuré</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>70 g blandet salat</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>800 g hakkede tomater på dåse</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>Grøntsager eller salat</td></tr> <tr><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 dl mild chilipasta</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 spsk hvedemel</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 spsk olivenolie, til stegning</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>1 spsk smør eller olie til stegning</td></tr> <tr><td class="cb"><input type="checkbox"></td><td>1 spsk smør eller olie 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>2 håndfulde frisk dild, til pynt</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2 spsk finvalset havregryn</td></tr> <tr><td class="cb"><input type="checkbox"></td><td>2 spsk finvalset havregryn</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2 spsk hvedemel</td></tr> <tr><td class="cb"><input type="checkbox"></td><td>2 spsk hvedemel</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>2 spsk olie til stegning</td></tr> <tr><td class="cb"><input type="checkbox"></td><td>2 spsk olivenolie</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>3 spsk hvedemel</td></tr> <tr><td class="cb"><input type="checkbox"></td><td>3 dl basmati ris</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>3 spsk olivenolie, til stegning</td></tr> <tr><td class="cb"><input type="checkbox"></td><td>3 spsk olivenolie, til stegning</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>Frisk timian eller persille</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>Friskmalet peber</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>Friskmalet sort peber rigeligt</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>Ris eller kartofler</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>salt og friskkværnet peber</td></tr> <tr><td class="cb"><input type="checkbox"></td><td>salt og friskkværnet 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><td class="cb"><input type="checkbox"></td><td>Salt til pastavand</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 fed hvidløg, 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 kg kylling, overlår og evt bryst med skind</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 spsk frisk rosmarin, 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>2 spsk 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>200 g pancetta eller røget bacon i tern</td></tr> <tr><td class="cb"><input type="checkbox"></td><td>2 spsk persille, finthakket</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>4 fed hvidløg, finthakket</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>4 laksefileter med skind (ca. 150 g pr. stk)</td></tr> <tr><td class="cb"><input type="checkbox"></td><td>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>500 g hakket svinekød</td></tr> <tr><td class="cb"><input type="checkbox"></td><td>500 g hakket svinekød</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>600 g kyllingebryst, skåret i grove tern</td></tr>
<tr><td class="cb"><input type="checkbox"></td><td>75 g bacon, i skiver</td></tr>
<tr><th colspan="2" class="cat">Mejeri & Æg</th></tr>
<tr><td class="cb"><input type="checkbox"></td><td>125 g frisk mozzarella</td></tr>
</table> </table>
</body> </body>
</html> </html>
+132 -36
View File
@@ -1,6 +1,10 @@
{ {
"count": 45, "count": 69,
"items": [ "items": [
{
"name": "0,50 dl hvidvin",
"category": "andet"
},
{ {
"name": "0,50 tsk stødt spidskommen", "name": "0,50 tsk stødt spidskommen",
"category": "andet" "category": "andet"
@@ -14,39 +18,55 @@
"category": "andet" "category": "andet"
}, },
{ {
"name": "1 dl grøntsagsbouillon", "name": "1 dl cremefraiche 18 %",
"category": "andet" "category": "andet"
}, },
{ {
"name": "1 dl mælk", "name": "1 dl mælk",
"category": "andet" "category": "andet"
}, },
{
"name": "1 dl rødvin, eller grøntsagsboullion",
"category": "andet"
},
{ {
"name": "1 fed hvidløg, presset", "name": "1 fed hvidløg, presset",
"category": "andet" "category": "andet"
}, },
{ {
"name": "1 hel knoldselleri (ca. 700-800 g)", "name": "1 knivspids muskatnød, fintrevet",
"category": "andet" "category": "andet"
}, },
{ {
"name": "1 spsk smør, til at smøre fadet", "name": "1 knivspids røget paprika",
"category": "andet"
},
{
"name": "1 knivspids sød paprika",
"category": "andet" "category": "andet"
}, },
{ {
"name": "1 spsk smør, til stegning", "name": "1 spsk smør, til stegning",
"category": "andet" "category": "andet"
}, },
{
"name": "1 squash, groftrevet",
"category": "andet"
},
{
"name": "1 tsk sød paprika",
"category": "andet"
},
{ {
"name": "1 tsk tørret timian", "name": "1 tsk tørret timian",
"category": "andet" "category": "andet"
}, },
{ {
"name": "100 g parmesan, fintrevet", "name": "100 g rejer",
"category": "andet" "category": "andet"
}, },
{ {
"name": "2 fed hvidløg", "name": "100 g stenbiderrogn",
"category": "andet" "category": "andet"
}, },
{ {
@@ -54,19 +74,43 @@
"category": "andet" "category": "andet"
}, },
{ {
"name": "4 æggeblommer + 1 helt æg", "name": "2 spsk smør",
"category": "andet" "category": "andet"
}, },
{ {
"name": "4 gulerødder, skrællede og skiveskåret", "name": "2 tsk tørret oregano",
"category": "andet" "category": "andet"
}, },
{ {
"name": "400 g spaghetti eller rigatoni", "name": "200 g lasagneplader",
"category": "andet" "category": "andet"
}, },
{ {
"name": "50 g pecorino, fintrevet (kan erstattes af mere parmesan)", "name": "3 dl mælk",
"category": "andet"
},
{
"name": "4 grønne asparges",
"category": "andet"
},
{
"name": "4 gulerødder, groftrevet",
"category": "andet"
},
{
"name": "4 hvide asparges",
"category": "andet"
},
{
"name": "4 skiver brød",
"category": "andet"
},
{
"name": "5 stængler bladselleri, groftrevet",
"category": "andet"
},
{
"name": "50 g rasp",
"category": "andet" "category": "andet"
}, },
{ {
@@ -74,31 +118,79 @@
"category": "andet" "category": "andet"
}, },
{ {
"name": "800 g kartofler, skrubbede", "name": "8 rødspættefilet",
"category": "andet" "category": "andet"
}, },
{ {
"name": "½ citron", "name": "2 spsk mayonnaise",
"category": "frugt & grønt" "category": "frost"
},
{
"name": "2,50 dl piskefløde",
"category": "frost"
}, },
{ {
"name": "1 citron saft og fintrevet skal", "name": "1 citron saft og fintrevet skal",
"category": "frugt & grønt" "category": "frugt & grønt"
}, },
{ {
"name": "Grøn salat", "name": "1 citron, 1 spsk saft herfra",
"category": "frugt & grønt"
},
{
"name": "1 citron, skåret i både",
"category": "frugt & grønt"
},
{
"name": "12 skiver agurk",
"category": "frugt & grønt"
},
{
"name": "2 banan, skåret i skiver på ca. 1 cm",
"category": "frugt & grønt"
},
{
"name": "2 spsk koncentreret tomatpuré",
"category": "frugt & grønt"
},
{
"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" "category": "frugt & grønt"
}, },
{ {
"name": "Grøntsager eller salat", "name": "Grøntsager eller salat",
"category": "frugt & grønt" "category": "frugt & grønt"
}, },
{
"name": "1 dl mild chilipasta",
"category": "kolonial"
},
{
"name": "1 spsk hvedemel",
"category": "kolonial"
},
{
"name": "1 spsk olivenolie, til stegning",
"category": "kolonial"
},
{ {
"name": "1 spsk smør eller olie til stegning", "name": "1 spsk smør eller olie til stegning",
"category": "kolonial" "category": "kolonial"
}, },
{ {
"name": "1 tsk salt", "name": "2 håndfulde frisk dild, til pynt",
"category": "kolonial" "category": "kolonial"
}, },
{ {
@@ -110,11 +202,11 @@
"category": "kolonial" "category": "kolonial"
}, },
{ {
"name": "2 spsk olie til stegning", "name": "2 spsk olivenolie",
"category": "kolonial" "category": "kolonial"
}, },
{ {
"name": "3 spsk hvedemel", "name": "3 dl basmati ris",
"category": "kolonial" "category": "kolonial"
}, },
{ {
@@ -122,15 +214,7 @@
"category": "kolonial" "category": "kolonial"
}, },
{ {
"name": "Frisk timian eller persille", "name": "50 g saltede peanuts",
"category": "kolonial"
},
{
"name": "Friskmalet peber",
"category": "kolonial"
},
{
"name": "Friskmalet sort peber rigeligt",
"category": "kolonial" "category": "kolonial"
}, },
{ {
@@ -145,24 +229,16 @@
"name": "Salt og peber", "name": "Salt og peber",
"category": "kolonial" "category": "kolonial"
}, },
{
"name": "Salt til pastavand",
"category": "kolonial"
},
{ {
"name": "1 fed hvidløg, finthakket", "name": "1 fed hvidløg, finthakket",
"category": "kød & fisk" "category": "kød & fisk"
}, },
{
"name": "1 kg kylling, overlår og evt bryst med skind",
"category": "kød & fisk"
},
{ {
"name": "1 løg, finthakket", "name": "1 løg, finthakket",
"category": "kød & fisk" "category": "kød & fisk"
}, },
{ {
"name": "1 spsk frisk rosmarin, finthakket", "name": "2 løg, finthakket",
"category": "kød & fisk" "category": "kød & fisk"
}, },
{ {
@@ -170,16 +246,36 @@
"category": "kød & fisk" "category": "kød & fisk"
}, },
{ {
"name": "200 g pancetta eller røget bacon i tern", "name": "2 spsk persille, finthakket",
"category": "kød & fisk"
},
{
"name": "4 fed hvidløg, finthakket",
"category": "kød & fisk" "category": "kød & fisk"
}, },
{ {
"name": "4 laksefileter med skind (ca. 150 g pr. stk)", "name": "4 laksefileter med skind (ca. 150 g pr. stk)",
"category": "kød & fisk" "category": "kød & fisk"
}, },
{
"name": "400 g hakket oksekød",
"category": "kød & fisk"
},
{ {
"name": "500 g hakket svinekød", "name": "500 g hakket svinekød",
"category": "kød & fisk" "category": "kød & fisk"
},
{
"name": "600 g kyllingebryst, skåret i grove tern",
"category": "kød & fisk"
},
{
"name": "75 g bacon, i skiver",
"category": "kød & fisk"
},
{
"name": "125 g frisk mozzarella",
"category": "mejeri & æg"
} }
] ]
} }
+1 -1
View File
@@ -1 +1 @@
{"count": 7, "items": [{"date": "2026-05-07", "recipe": {"name": "Macaroni and cheese", "slug": "macaroni-and-cheese"}}, {"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"}}, {"date": "2026-05-08", "recipe": {"name": "Ribeye steak med bearnaise og ovnfritter", "slug": "ribeye-steak-med-bearnaise-og-ovnfritter-2"}}, {"date": "2026-05-06", "recipe": {"name": "Rester: Frikadeller fra mandag", "slug": ""}}, {"date": "2026-05-05", "recipe": {"name": "Rester: Bagt kylling fra s\u00f8ndag", "slug": ""}}]} {"count": 7, "items": [{"date": "2026-05-23", "recipe": {"name": "Ingen hjemme", "slug": ""}}, {"date": "2026-05-24", "recipe": {"name": "Kylling i cremet sennepssauce", "slug": "kylling-i-cremet-sennepssauce"}}, {"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"}}]}