diff --git a/README.md b/README.md index e69de29..41bf427 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,3 @@ +# Home Assistant Playground + +Tinkering place to extend, amend and try out different aspects of Home Assistant like configuration, blueprints, … \ No newline at end of file diff --git a/blueprints/virtual-sunrise.yaml b/blueprints/virtual-sunrise.yaml new file mode 100644 index 0000000..175c4f5 --- /dev/null +++ b/blueprints/virtual-sunrise.yaml @@ -0,0 +1,349 @@ +blueprint: + name: Wake-up Sunrise (Kelvin, smooth & smart) - Fixed + description: > + Moderner Wecker mit Sonnenaufgangseffekt. Korrigierte Version mit sequenzieller Variablen-Berechnung. + domain: automation + + input: + light_entity: + name: Ziel-Licht + selector: + entity: + domain: light + timestamp_sensor: + name: Zeitstempel-Entität (optional) + default: none + selector: + entity: + device_class: timestamp + manual_time: + name: Manuelle Weckzeit + default: "07:00:00" + selector: + time: {} + sunrise_duration_min: + name: Dauer des Sonnenaufgangs (Minuten) + default: 25 + selector: + number: + min: 5 + max: 120 + step: 5 + unit_of_measurement: min + mode: slider + start_brightness_pct: + name: Starthelligkeit (%) + default: 3 + selector: + number: + min: 1 + max: 100 + step: 1 + mode: slider + end_brightness_pct: + name: Endhelligkeit (%) + default: 100 + selector: + number: + min: 5 + max: 100 + step: 1 + mode: slider + start_kelvin: + name: Start-Kelvin (warm) + default: 2200 + selector: + number: + min: 1500 + max: 9000 + step: 50 + unit_of_measurement: K + mode: slider + end_kelvin: + name: End-Kelvin (kühler / tageslicht) + default: 6500 + selector: + number: + min: 1500 + max: 9000 + step: 50 + unit_of_measurement: K + mode: slider + enable_color_ramp: + name: Farbverlauf (RGB) statt Kelvin + default: false + selector: + boolean: {} + start_color_rgb: + name: Startfarbe (RGB) + default: { r: 255, g: 120, b: 20 } + selector: + color_rgb: {} + end_color_rgb: + name: Endfarbe (RGB) + default: { r: 255, g: 255, b: 255 } + selector: + color_rgb: {} + ramp_steps: + name: Schrittanzahl (Glätte) + default: 60 + selector: + number: + min: 10 + max: 240 + step: 10 + check_entity: + name: Check-Entität + default: none + selector: + entity: {} + require_workday: + name: Nur an Arbeitstagen + default: false + selector: + boolean: {} + workday_sensor: + name: Workday-Sensor + default: binary_sensor.workday + selector: + entity: + domain: binary_sensor + respect_quiet_hours: + name: Ruhezeiten beachten + default: false + selector: + boolean: {} + quiet_start: + name: Ruhezeit Beginn + default: "22:00:00" + selector: + time: {} + quiet_end: + name: Ruhezeit Ende + default: "06:00:00" + selector: + time: {} + off_cancels: + name: Ausschalten bricht ab + default: true + selector: + boolean: {} + pre_actions: + name: Vor-Aktionen + default: [] + selector: + action: {} + post_actions: + name: Nach-Aktionen + default: [] + selector: + action: {} + +# Globale Variablen (nur statische Zuweisungen aus Inputs) +variables: + le: !input light_entity + sensor_ts: !input timestamp_sensor + manual_time: !input manual_time + duration_min: !input sunrise_duration_min + start_pct: !input start_brightness_pct + end_pct: !input end_brightness_pct + steps: !input ramp_steps + enable_rgb: !input enable_color_ramp + off_cancels: !input off_cancels + check_entity: !input check_entity + require_workday: !input require_workday + workday_sensor: !input workday_sensor + respect_quiet: !input respect_quiet_hours + quiet_start: !input quiet_start + quiet_end: !input quiet_end + startK_in: !input start_kelvin + endK_in: !input end_kelvin + rgb_start: !input start_color_rgb + rgb_end: !input end_color_rgb + +trigger: + - platform: time_pattern + minutes: "*" + +action: + # Ebene 1: Basis-Berechnungen und Geräte-Attribute holen + - variables: + seconds: "{{ (duration_min | float(25)) * 60 }}" + range_pct: "{{ (end_pct | float) - (start_pct | float) }}" + minK_native: "{{ state_attr(le, 'min_color_temp_kelvin') }}" + maxK_native: "{{ state_attr(le, 'max_color_temp_kelvin') }}" + minM: "{{ state_attr(le, 'min_mireds') }}" + maxM: "{{ state_attr(le, 'max_mireds') }}" + sr: "{{ rgb_start.r | int }}" + sg: "{{ rgb_start.g | int }}" + sb: "{{ rgb_start.b | int }}" + er: "{{ rgb_end.r | int }}" + eg: "{{ rgb_end.g | int }}" + eb: "{{ rgb_end.b | int }}" + supported_modes: "{{ state_attr(le, 'supported_color_modes') | default([]) }}" + + # Ebene 2: Berechnungen, die auf Ebene 1 basieren (z.B. device_k_min) + - variables: + device_k_min: >- + {% if minK_native is number %}{{ minK_native | int }} + {% elif maxM is number %}{{ (1000000 / (maxM | float)) | int }} + {% else %}2000{% endif %} + device_k_max: >- + {% if maxK_native is number %}{{ maxK_native | int }} + {% elif minM is number %}{{ (1000000 / (minM | float)) | int }} + {% else %}6500{% endif %} + tick: >- + {% set t = (seconds | float) / (steps | float) %} + {{ [ t, 1 ] | max | int }} + can_rgb: >- + {{ 'hs' in supported_modes or 'rgb' in supported_modes or 'rgbw' in supported_modes or 'rgbww' in supported_modes or 'xy' in supported_modes }} + can_ct: >- + {{ 'color_temp' in supported_modes or minM is number or minK_native is number or maxM is number or maxK_native is number }} + + # Ebene 3: Finale Kelvin-Werte und Bedingungen + - variables: + startK_clamped: >- + {% set s1 = [startK_in | int, device_k_min] | max %} + {{ [s1, device_k_max] | min }} + endK_clamped: >- + {% set e1 = [endK_in | int, device_k_min] | max %} + {{ [e1, device_k_max] | min }} + cond_check_entity_ok: >- + {{ check_entity == 'none' or states(check_entity) in ['on','home','unknown'] }} + cond_workday_ok: >- + {{ (not require_workday) or (is_state(workday_sensor, 'on')) }} + cond_quiet_ok: >- + {% if not respect_quiet %}true + {% else %} + {% set nowt = now().time() %} + {% set qs = strptime(quiet_start, '%H:%M:%S').time() %} + {% set qe = strptime(quiet_end, '%H:%M:%S').time() %} + {% if qs <= qe %}{{ not (nowt >= qs and nowt < qe) }} + {% else %}{{ not (nowt >= qs or nowt < qe) }}{% endif %} + {% endif %} + + # Ebene 4: Finalisierung der Kelvin-Werte + - variables: + start_kelvin_final: "{{ [startK_clamped, endK_clamped] | min }}" + end_kelvin_final: "{{ [startK_clamped, endK_clamped] | max }}" + + # --- Ablauf --- + - wait_template: >- + {{ sensor_ts == 'none' or as_timestamp(states(sensor_ts), None) != None }} + + - wait_template: >- + {% set alarm_ts = as_timestamp(sensor_ts != 'none' and states(sensor_ts) or (states('sensor.date') ~ ' ' ~ manual_time)) %} + {% set now_ts = as_timestamp(now()) %} + {{ 0 < (alarm_ts - now_ts) <= (seconds | float) and cond_check_entity_ok and cond_workday_ok and cond_quiet_ok }} + + - choose: [] + default: !input pre_actions + + - condition: template + value_template: "{{ cond_check_entity_ok and cond_workday_ok and cond_quiet_ok }}" + + - choose: + - conditions: "{{ enable_rgb and can_rgb }}" + sequence: + - service: light.turn_on + target: + entity_id: !input light_entity + data: + brightness_pct: "{{ start_pct | int }}" + rgb_color: ["{{ sr }}","{{ sg }}","{{ sb }}"] + transition: 1 + - conditions: "{{ can_ct }}" + sequence: + - service: light.turn_on + target: + entity_id: !input light_entity + data: + brightness_pct: "{{ start_pct | int }}" + color_temp_kelvin: "{{ start_kelvin_final | int }}" + transition: 1 + default: + - service: light.turn_on + target: + entity_id: !input light_entity + data: + brightness_pct: "{{ start_pct | int }}" + transition: 1 + + - repeat: + while: + - >- + {% set alarm_ts = as_timestamp(sensor_ts != 'none' and states(sensor_ts) or (states('sensor.date') ~ ' ' ~ manual_time)) %} + {{ 0 < (alarm_ts - as_timestamp(now())) <= (seconds | float) }} + - "{{ not (off_cancels and is_state(le, 'off')) }}" + sequence: + - delay: + seconds: "{{ tick | int }}" + - variables: + alarm_ts: >- + {{ as_timestamp(sensor_ts != 'none' and states(sensor_ts) or (states('sensor.date') ~ ' ' ~ manual_time)) }} + remain: "{{ (alarm_ts - as_timestamp(now())) | float }}" + frac: "{{ (remain / (seconds | float)) | float }}" + bri_now: "{{ ((end_pct | float) - ((range_pct | float) * frac)) | round(0) | int }}" + r_now: "{{ (sr + (er - sr) * (1 - frac)) | round(0) | int }}" + g_now: "{{ (sg + (eg - sg) * (1 - frac)) | round(0) | int }}" + b_now: "{{ (sb + (eb - sb) * (1 - frac)) | round(0) | int }}" + kelv_now: "{{ ((end_kelvin_final | float) - ((end_kelvin_final | float - start_kelvin_final | float) * frac)) | round(0) | int }}" + - choose: + - conditions: "{{ enable_rgb and can_rgb }}" + sequence: + - service: light.turn_on + target: + entity_id: !input light_entity + data: + brightness_pct: "{{ [bri_now, 1] | max }}" + rgb_color: ["{{ r_now }}","{{ g_now }}","{{ b_now }}"] + transition: "{{ [ (tick | int) - 1, 0 ] | max }}" + - conditions: "{{ can_ct }}" + sequence: + - service: light.turn_on + target: + entity_id: !input light_entity + data: + brightness_pct: "{{ [bri_now, 1] | max }}" + color_temp_kelvin: "{{ kelv_now }}" + transition: "{{ [ (tick | int) - 1, 0 ] | max }}" + default: + - service: light.turn_on + target: + entity_id: !input light_entity + data: + brightness_pct: "{{ [bri_now, 1] | max }}" + transition: "{{ [ (tick | int) - 1, 0 ] | max }}" + + - choose: + - conditions: "{{ enable_rgb and can_rgb }}" + sequence: + - service: light.turn_on + target: + entity_id: !input light_entity + data: + brightness_pct: "{{ end_pct | int }}" + rgb_color: ["{{ er }}","{{ eg }}","{{ eb }}"] + transition: 1 + - conditions: "{{ can_ct }}" + sequence: + - service: light.turn_on + target: + entity_id: !input light_entity + data: + brightness_pct: "{{ end_pct | int }}" + color_temp_kelvin: "{{ end_kelvin_final | int }}" + transition: 1 + default: + - service: light.turn_on + target: + entity_id: !input light_entity + data: + brightness_pct: "{{ end_pct | int }}" + transition: 1 + + - choose: [] + default: !input post_actions + +mode: single +max_exceeded: silent \ No newline at end of file