Zonwering met hysterese in Home Assistant: waarom mijn screens de hele dag open en dicht gingen

·

Illustratie van een raam met half neergelaten screen, daarboven drie lagen met een schild, een zon met thermometer en hysteresecurve, en een hand met klok, naast een thermometer, windzak en regenwolk

Zonwering in Home Assistant automatiseren lijkt simpel: dicht als de zon op het huis staat, open bij regen en wind. Maar mijn screens gingen in oktober de hele dag open en dicht, en de oorzaak zat niet in een sensor maar in de opzet van de automatisering. In dit stuk laat ik zien hoe de oude versie eruitzag, waarom die niet kon werken, en hoe ik hem heb herbouwd met hysterese, een aparte veiligheidslaag en een handmatige stand die vanzelf verloopt.

Dit is een vervolg op Somfy-screens in Home Assistant, waarin ik beschrijf hoe onze Hardol-screens via de Connexoon in Home Assistant terechtkwamen.

Het probleem

De zonwering moest simpel zijn: dicht als de zon op het huis staat en het binnen warm wordt, open bij regen, wind en ’s nachts. Dat werkte in de zomer redelijk. Zodra de verwarming in het najaar aanging, begon het gedoe: schermen die om de paar minuten van stand wisselden, een gezin dat ze handmatig omhoog deed en ze vijf minuten later weer dicht zag gaan, en een logboek vol cover.close_cover-aanroepen.

Een thermostaat die in stappen van 0,5 K meet, een warmtepomp die naar 20 °C regelt en een automatisering met een harde grens op 20,5 °C zijn samen een oscillator. Niet één onderdeel was kapot; de combinatie was het probleem.

De oude automatisering

Dit was de kern. Elke vijf minuten en bij elke sensorwijziging werden variabelen berekend, en werd via choose óf open_cover óf close_cover gestuurd.

triggers:
  - trigger: time_pattern
    minutes: /5
  - trigger: state
    entity_id:
      - sensor.binnentemperatuur
      - sensor.neerslagintensiteit
      - sensor.windsnelheid
      - sun.sun
actions:
  - variables:
      temp: "{{ states('sensor.binnentemperatuur') | float(0) }}"
      regen: "{{ states('sensor.neerslagintensiteit') | float(0) }}"
      wind: "{{ states('sensor.windsnelheid') | float(99) }}"
      azimuth: "{{ state_attr('sun.sun', 'azimuth') | float(0) }}"
      nacht: "{{ is_state('sun.sun', 'below_horizon') }}"
  - choose:
      - conditions: "{{ nacht or regen > 0 or wind > 15 or temp < 20.5 }}"
        sequence:
          - action: cover.open_cover
            target: { entity_id: cover.all_screens }
      - conditions: "{{ temp > 20 and 60 <= azimuth <= 300 and regen < 1 and wind < 6 and not nacht }}"
        sequence:
          - action: cover.close_cover
            target: { entity_id: cover.all_screens }
mode: restart

Het leest logisch, maar er zitten vijf fouten in die elkaar versterken.

  1. Geen hysterese. Dicht bij temp > 20, open bij temp < 20.5: dat is één harde grens op 20,5 °C. Met een thermostaat die in stappen van 0,5 K meet en een verwarming die naar 20 °C regelt, pendelt de meting structureel tussen 20,0 en 20,5. Elke stap is een commando naar alle schermen.
  2. Stateless. De automatisering kijkt nooit of de schermen al in de gewenste stand staan. Elke run stuurt een commando, ook als er niets verandert. Handmatig ingrijpen is daardoor onmogelijk: binnen vijf minuten wordt het teruggedraaid.
  3. Trigger op sun.sun zonder to:. Een state-trigger zonder to: reageert ook op wijzigingen van attributen. De zon-entiteit werkt azimut en elevatie om de paar minuten bij, dus de automatisering draaide veel vaker dan de bedoeling was.
  4. Windgrenzen zonder eenheid. Dicht alleen onder 6, open boven 15. In km/h is 6 bijna windstil en gaan de schermen zelden dicht; in m/s is 15 (7 Bft) veel te hoog als veiligheidsgrens. Fabrikanten zitten rond 10 m/s.
  5. Zon alleen op azimut. 60–300° is in Nederland de hele dag. Zonder elevatie of een zonkenmerk gaan de schermen op een bewolkte dag dicht zodra het binnen 20,5 °C is, en blokkeren ze in de winter gratis zonnewarmte.

Kleiner, maar niet onbelangrijk: regen | float(0) telt een onbeschikbare regensensor als droog, regen > 0 reageert op meetruis van 0,01 mm/u, en mode: restart voegt bij losse service-calls niets toe.

De aanpak: drie lagen

De oplossing is niet een slimmere choose, maar het uit elkaar trekken van drie vragen die in de oude versie door elkaar liepen:

  • Veiligheid: is het veilig om de schermen beneden te hebben?
  • Comfort: wil ik ze beneden hebben?
  • Aansturing: moet er nu een commando gestuurd worden?
binary_sensor.zonwering_blokkade  (veiligheid: wind, regen, nacht) ──┐
binary_sensor.zonwering_gewenst   (comfort: zon, temperatuur)     ──┼──> automatisering ──> cover.all_screens
input_boolean.zonwering_handmatig (vervalt na 3 uur)              ──┘
                          veiligheid gaat altijd vóór handmatig; comfort niet

De twee binary_sensors rekenen de beslissing uit en bewaren hun eigen toestand. Daardoor wordt hysterese mogelijk. De automatisering wordt dom en voorspelbaar: ze triggert op een wissel, vergelijkt met de werkelijke stand van cover.all_screens en stuurt alleen als die afwijkt. De handmatige stand zit ertussen en blokkeert alleen de comfortlaag, nooit de veiligheid.

Het package

Alles staat in één packagebestand, zonwering_automatisch_regelen.yaml. Vervang de drie sensornamen en cover.all_screens door je eigen entiteiten.

Helpers

De drempels staan in input_number-helpers, zodat je ze op het dashboard kunt bijstellen zonder YAML aan te raken. De handmatige stand is een input_boolean.

input_boolean:
  zonwering_handmatig:
    name: Zonwering handmatig
    icon: mdi:hand-back-right

input_number:
  zonwering_binnen_dicht:
    name: Zonwering – binnen dicht vanaf (°C)
    min: 19
    max: 26
    step: 0.5
    initial: 22.0
    unit_of_measurement: "°C"
  zonwering_binnen_open:
    name: Zonwering – binnen weer open onder (°C)
    min: 18
    max: 25
    step: 0.5
    initial: 21.0
    unit_of_measurement: "°C"
  zonwering_wind_max:
    name: Zonwering – wind open boven (m/s)
    min: 4
    max: 20
    step: 0.5
    initial: 10
    unit_of_measurement: "m/s"

Laag 1: veiligheid

binary_sensor.zonwering_blokkade is aan bij wind, regen of nacht. Onbeschikbare sensoren krijgen een onveilige standaardwaarde (float(99)), zodat een uitgevallen sensor altijd tot “open” leidt. De wind heeft hysterese via this.state: aan boven de maximumwaarde, pas weer uit onder 70 % daarvan. Een delay_off van tien minuten voorkomt dat één windstille minuut de schermen weer naar beneden stuurt.

template:
  - binary_sensor:
      - name: Zonwering blokkade
        unique_id: zonwering_blokkade
        icon: mdi:weather-windy
        delay_off: "00:10:00"
        state: >
          {% set wind  = states('sensor.windsnelheid') | float(99) %}
          {% set regen = states('sensor.neerslagintensiteit') | float(99) %}
          {% set elev  = state_attr('sun.sun', 'elevation') | float(-10) %}
          {% set wind_max = states('input_number.zonwering_wind_max') | float(10) %}
          {% set al_aan = this.state == 'on' if this is defined else false %}
          {% set wind_grens = (wind_max * 0.7) if al_aan else wind_max %}
          {{ wind > wind_grens or regen > 0.2 or elev < 3 }}

Laag 2: comfort

binary_sensor.zonwering_gewenst beantwoordt één vraag: wil ik de schermen nu dicht? Dicht vanaf 22,0 °C, pas weer open onder 21,0 °C: een band van twee thermostaatstappen, ruim boven het stooksetpoint van 20 °C. De zon moet minstens 10° boven de horizon staan, de ochtendrust tot 08:30 blijft, en de blokkade moet uit zijn. delay_on en delay_off werken als debounce.

      - name: Zonwering gewenst
        unique_id: zonwering_gewenst
        icon: mdi:blinds
        delay_on: "00:03:00"
        delay_off: "00:05:00"
        state: >
          {% set temp  = states('sensor.binnentemperatuur') | float(0) %}
          {% set elev  = state_attr('sun.sun', 'elevation') | float(0) %}
          {% set az    = state_attr('sun.sun', 'azimuth') | float(0) %}
          {% set dicht = states('input_number.zonwering_binnen_dicht') | float(22) %}
          {% set open  = states('input_number.zonwering_binnen_open') | float(21) %}
          {% set al_aan = this.state == 'on' if this is defined else false %}
          {% set ochtendrust_voorbij = now().hour * 60 + now().minute >= 8 * 60 + 30 %}
          {% set zon_op_huis = elev > 10 and 60 <= az <= 300 %}
          {% set warm = temp >= dicht if not al_aan else temp > open %}
          {{ ochtendrust_voorbij and zon_op_huis and warm
             and is_state('binary_sensor.zonwering_blokkade', 'off') }}

De regel warm = temp >= dicht if not al_aan else temp > open is de hele hysterese. Staat de sensor uit, dan moet de temperatuur de bovengrens halen. Staat hij aan, dan blijft hij aan tot de ondergrens.

Laag 3: aansturing

De automatisering triggert alleen op een wissel van de twee binary_sensors, op het verlopen van de handmatige stand en bij het opstarten van Home Assistant. Elk kwartier is er een vangnet, maar dat stuurt alleen als de werkelijke stand afwijkt. Veiligheid gaat vóór de handmatige stand; comfort niet.

automation:
  - alias: Zonwering Automatisch Regelen
    id: zonwering_automatisch_regelen
    mode: queued
    max: 3
    triggers:
      - trigger: state
        entity_id: binary_sensor.zonwering_blokkade
        to: ["on", "off"]
      - trigger: state
        entity_id: binary_sensor.zonwering_gewenst
        to: ["on", "off"]
      - trigger: state
        entity_id: input_boolean.zonwering_handmatig
        to: "off"
      - trigger: homeassistant
        event: start
      - trigger: time_pattern
        minutes: "/15"
    actions:
      - choose:
          # A. Veiligheid: altijd open, ook bij handmatig
          - conditions:
              - condition: state
                entity_id: binary_sensor.zonwering_blokkade
                state: "on"
              - condition: not
                conditions:
                  - condition: state
                    entity_id: cover.all_screens
                    state: open
            sequence:
              - action: cover.open_cover
                target: { entity_id: cover.all_screens }
          # B. Comfort: dicht
          - conditions:
              - condition: state
                entity_id: binary_sensor.zonwering_blokkade
                state: "off"
              - condition: state
                entity_id: input_boolean.zonwering_handmatig
                state: "off"
              - condition: state
                entity_id: binary_sensor.zonwering_gewenst
                state: "on"
              - condition: not
                conditions:
                  - condition: state
                    entity_id: cover.all_screens
                    state: closed
            sequence:
              - action: cover.close_cover
                target: { entity_id: cover.all_screens }
          # C. Comfort: open
          - conditions:
              - condition: state
                entity_id: binary_sensor.zonwering_blokkade
                state: "off"
              - condition: state
                entity_id: input_boolean.zonwering_handmatig
                state: "off"
              - condition: state
                entity_id: binary_sensor.zonwering_gewenst
                state: "off"
              - condition: not
                conditions:
                  - condition: state
                    entity_id: cover.all_screens
                    state: open
            sequence:
              - action: cover.open_cover
                target: { entity_id: cover.all_screens }

  - alias: Zonwering handmatig verloopt
    id: zonwering_handmatig_verloopt
    triggers:
      - trigger: state
        entity_id: input_boolean.zonwering_handmatig
        to: "on"
        for: "03:00:00"
    actions:
      - action: input_boolean.turn_off
        target: { entity_id: input_boolean.zonwering_handmatig }

De handmatige stand verloopt na drie uur. Dat is bewust: een override die je vergeet uit te zetten, is over een week een kapotte automatisering.

Testen voordat je het live zet

Een template die er goed uitziet, is nog geen template die werkt. Ik test de twee binary_sensors los van Home Assistant met een klein Python-script en Jinja2, waarin states(), state_attr(), now() en this.state nagebootst worden. Zo reken je in een paar seconden tientallen situaties door, inclusief de randgevallen die je in het echt pas na weken tegenkomt.

Dit zijn de scenario’s die ik heb gedraaid. Alle negentien gaven het verwachte resultaat.

ScenarioSensorWasVerwacht
Rustig weer, overdagblokkadeuituit
Wind 11 m/sblokkadeuitaan
Wind 8 m/sblokkadeaanblijft aan
Wind 6,5 m/sblokkadeaanuit
Wind 8 m/sblokkadeuitblijft uit
Regen 0,1 mm/u (ruis)blokkadeuituit
Regen 0,5 mm/ublokkadeuitaan
Windsensor unavailableblokkadeuitaan
Zon onder horizonblokkadeuitaan
Binnen 20,5 °C (stooksetpoint)gewenstuituit
Binnen 22,0 °Cgewenstuitaan
Binnen 21,5 °Cgewenstaanblijft aan
Binnen 21,0 °Cgewenstaanuit
Binnen 21,5 °Cgewenstuitblijft uit
Binnen 23 °C om 08:15gewenstuituit
Binnen 23 °C om 08:30gewenstuitaan
Binnen 23 °C, zon 5° hooggewenstuituit
Binnen 23 °C, blokkade aangewenstuituit
Temperatuursensor unavailablegewenstaanuit

De kern van het testscript:

import jinja2, datetime, yaml

class This:
    def __init__(self, state): self.state = state

def run(template, states, attrs, this_state, hour=12, minute=0):
    env = jinja2.Environment()
    env.globals['states'] = lambda e: states.get(e, 'unavailable')
    env.globals['state_attr'] = lambda e, a: attrs.get((e, a))
    env.globals['is_state'] = lambda e, v: states.get(e) == v
    env.globals['now'] = lambda: datetime.datetime(2026, 10, 8, hour, minute)
    def fl(v, default=0):
        try: return float(v)
        except (TypeError, ValueError): return default
    env.filters['float'] = fl
    return env.from_string(template).render(this=This(this_state)).strip()

pkg = yaml.safe_load(open('zonwering_automatisch_regelen.yaml'))
gewenst = next(b['state'] for b in pkg['template'][0]['binary_sensor']
               if b['name'] == 'Zonwering gewenst')
basis = {'sensor.windsnelheid': '4', 'sensor.neerslagintensiteit': '0',
         'input_number.zonwering_wind_max': '10',
         'input_number.zonwering_binnen_dicht': '22',
         'input_number.zonwering_binnen_open': '21',
         'binary_sensor.zonwering_blokkade': 'off'}
zon = {('sun.sun', 'elevation'): 25, ('sun.sun', 'azimuth'): 180}

# hysterese omhoog: 21,5 vanuit 'uit' blijft uit, vanuit 'aan' blijft aan
assert run(gewenst, {**basis, 'sensor.binnentemperatuur': '21.5'}, zon, 'off') == 'False'
assert run(gewenst, {**basis, 'sensor.binnentemperatuur': '21.5'}, zon, 'on') == 'True'

Daarna in Home Assistant zelf: controleer de configuratie, herlaad de template-entiteiten, en laat het package een dag draaien met de automatisering uit. Je ziet dan in de geschiedenis van binary_sensor.zonwering_gewenst of hij zich gedraagt zoals je wilt, zonder dat er een scherm beweegt. Pas daarna zet je de automatisering aan.

Wat ik hiervan heb geleerd

De drie principes die dit package robuust maken, gelden voor bijna elke automatisering die iets fysieks aanstuurt.

  1. Reken de beslissing uit in een template-sensor, niet in de automatisering. Een binary_sensor met this.state kan hysterese, heeft gratis geschiedenis in het dashboard en is los te testen. De automatisering wordt dan triviaal: volg de sensor.
  2. Stuur alleen bij een verschil. Vergelijk de gewenste stand met de werkelijke stand en doe niets als die gelijk zijn. Dat maakt handmatig ingrijpen mogelijk en houdt je logboek leesbaar.
  3. Scheid veiligheid van comfort. Wind en regen mogen nooit door een comfortregel of een handmatige stand overstemd worden. Twee sensors, twee prioriteiten.

Hetzelfde principe zit in mijn kamerthermostaat voor de warmtepomp: een hysterese van 0,5 K en minimale aan- en uittijden, omdat de thermostaten in stappen van 0,5 K meten.

Wat nog komt

  • Het actuele vermogen van de zonnepanelen als zonkenmerk in zonwering_gewenst. Dat is een betere maat voor “de zon schijnt echt” dan alleen de stand van de zon, en de sensor is er al.
  • Een grens voor de buitentemperatuur, zodat de schermen op een zonnige winterdag open blijven en de warmtepomp de gratis warmte kan gebruiken.

Vragen of verbeteringen? Laat het weten in de reacties.


Dit werkt in mijn situatie: Hardol-screens met Somfy-motoren via een Connexoon, een cover-groep cover.all_screens en Home Assistant. Drempels, sensornamen en windgrenzen van je eigen screens kunnen anders zijn. Controleer het tegen je eigen documentatie en de opgave van de fabrikant.


Lees ook


Reacties

Geef een reactie

Je e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *