Ankündigung

Einklappen
Keine Ankündigung bisher.

HA - Templating mit Jinja2

Einklappen
X
 
  • Filter
  • Zeit
  • Anzeigen
Alles löschen
neue Beiträge

    HA - Templating mit Jinja2

    HA kann an vielen Stellen (z.B. Template-Sensor, Template-Trigger, Template fähige Karten) Code über Jinja2-Templating ausführen.

    Wer immer schon ein wenig über Templating lernen wollte, der kann diese Beispiele hier zum Üben verwenden.

    Ich hab mir aus der Anleitung die interessantesten Teile herausgesucht und Code-Snippets erstellt, die man 1:1 in den Template-Editor der Entwicklerwerkzeuge kopieren kann.

    Wer den noch nicht kennt, der hat wirklich etwas verpasst. Also dringend mal ansehen!



    Template-Editor.png

    ​Interessant ist auch der Teil über Reusing Templates, das ist die Umschreibung von Macros.

    Leider ist im Trigger-Teil der Automation-Trigger nur ein Subset verfügbar, gekennzeichnet mit: (nicht in Limited Templates)

    Das ist wohl auch ein Grund, warum ich lieber Template-Trigger verwende, die jedoch nicht direkt auf Ereignisse reagieren können wie die Automation.

    Ich verwende in den folgenden Code-Beispielen häufig Sensoren der Demo-Integration, also z.B. sensor.outside_temperature oder light.bed_light.

    Da man bei Strings sowohl einfache also auch doppelte Hochkomma verwenden kann, hab ich meist einfache verwendet, man sieht das auch häufiger so.
    Bei JSON-Strings oder Verschachtelungen muss man jedoch aufpassen, da es dort nicht mehr egal ist. (z.B. thread: "Walter's Anleitungen")

    Noch ein Hinweis zur String-Concatenation. Man sieht immer wieder dass statt dem Tilde-Operator der Plus-Operator zum verbinden von Strings verwendet wird.
    ~ gilt als der sichere und + gilt als der unsichere, und wenn man wissen möchte, warum sie so genannt werden, entfernt man einfach mal die string Umwandlung bei der unteren Zeile.

    HTML-Code:
    {{ "Temperatur: " ~ 22.5 ~ " °C" }}
    {{ "Temperatur: " + 22.5 | string + " °C" }}

    Die Reihenfolge hier entspricht der Original Doku, so findet man sich schneller zurecht wenn man beides parallel verwendet.


    States (nicht in Limited Templates)

    Die Funktion states() lernt man meist als erstes, with_unit bleibt dabei aber oft unbekannt.

    HTML-Code:
    {{ states('sensor.outside_temperature') }}
    {{ states('sensor.outside_temperature', with_unit=True) }}
    Interessant ist auch die Warnung, dass man besser states('sensor.temperature') verwendet und nicht states.sensor.temperature.state, obwohl beides funktioniert.


    Attributes (nicht in Limited Templates)

    state_attr() ist vermutlich ebenfalls bereits bekannt, da viele Entities nicht nur einen Status sondern auch noch zusätzliche Attribute hat.

    HTML-Code:
    {{ state_attr('sensor.outside_temperature','battery_level') }}

    State Translated (nicht in Limited Templates)

    HTML-Code:
    {{ states('sun.sun') }}
    {{ state_translated('sun.sun') }}
    {{ 'sun.sun' | state_translated }}
    state_translated() funktioniert leider nur bei manchen States und auch nicht bei Attributen.


    Working with Groups (nicht in Limited Templates)

    Die expand() Funktion liefert immer das gesamte Objekt zurück.

    HTML-Code:
    {{ expand('media_player.group') | selectattr('attributes.volume_level', 'gt', 0) | list }}
    Die Funktion kann auch mit mehr als einer Group umgehen: expand('group1', 'group2', ...)


    Entities (auch in Limited Templates)

    Mit is_hidden_entity(entity_id) kann man prüfen, ob eine Entity hidden ist.

    Liste aller hidden Entities

    HTML-Code:
    {{ states | map(attribute='entity_id') | select('is_hidden_entity') | list }}
    Mit reject statt select bekommt man alle non-hidden Entities.​
    Angehängte Dateien
    Zuletzt geändert von scw2wi; Gestern, 16:44.

    #2
    Devices

    In HA werden Entities optimalerweise zu Geräten gruppiert, und die device_id ist eine lange kryptische Zahl.

    device_name() findet das Gerät zu jeder Entität, wenn es eines gibt, oder auch den Namen zur kryptischen Zahl.
    device_id() zeigt die ID, egal ob man den Geräte-Namen oder eine Entität davon kennt.
    device_entities() liefert eine Liste aller Entitäten des Geräts. Hier muss man aber die ID kennen.

    HTML-Code:
    {{ device_name('sensor.system_monitor_belegter_massenspeicher') }}
    {{ device_id('sensor.system_monitor_belegter_massenspeicher') }}
    {{ device_name(device_id('sensor.system_monitor_belegter_massenspeicher')) }}
    {{ device_id(device_name('sensor.system_monitor_belegter_massenspeicher')) }}
    {{ device_entities(device_id('sensor.system_monitor_belegter_massenspeicher')) }}
    device_attr() liefert Geräte-Attribute. Dazu muss man aber den Attribut-Namen kennen und den findet man nur in der JSON-Datei.

    HTML-Code:
    {# 1. Finde die erste passende Entität für einen Fritzbox-Sensor #}
    {% set entity = states.sensor
      | selectattr('entity_id', 'search', 'fritz_box.*letzter_neustart')
      | map(attribute='entity_id') | first %}
    
    {% if entity is not none %}
      {% set dev_id = device_id(entity) %}
      
      Geräte-Details für: {{ device_attr(dev_id, 'name') }}
      - Device ID: {{ dev_id }}
      - Hersteller: {{ device_attr(dev_id, 'manufacturer') }}
      - Modell: {{ device_attr(dev_id, 'model') }}
      - SW-Version: {{ device_attr(dev_id, 'sw_version') }}
      - HW-Version: {{ device_attr(dev_id, 'hw_version') }}
      - Bereich (ID): {{ device_attr(dev_id, 'area_id') }}
      - Bereich (Name): {{ area_name(dev_id) }}
      - Konfigurations-URL: {{ device_attr(dev_id, 'configuration_url') }}
      - Verbindung via: {{ device_attr(dev_id, 'via_device_id') }}
      
      identifiers:
      {{ device_attr(dev_id, 'identifiers') }}
      
    {% else %}
      Keine Entität gefunden, die auf das Muster passt.
    {% endif %}
    Der folgende Code zeigt, von welcher Integration ein Sensor erstellt wurde.

    HTML-Code:
    {% set entity_id = 'sensor.outside_temperature' %}
    {% set device_id = device_id(entity_id) %}
    {% if device_id %}
      Integration: {{ device_attr(device_id, 'identifiers') | map('first') | first }}
    {% else %}
      Kein Gerät gefunden (evtl. ein manueller Template-Sensor).
    {% endif %}

    Config-Entries

    Jede Integration kann ein oder mehrere Instanzen erzeugen, dafür gibt es dann eine eindeutige config_entry_id, und diese Einträge kann man mit config_entry_id() und config_entry_attr() abfragen.

    Ich hab dazu aber kein für mich relevantes Code-Beispiel finden können. Da muss man wohl selber Integrationen entwickeln, um das zu benötigen.


    Floors & Areas

    Floor ist das Stockwerk und Area sind dann die Bereiche oder Räume in dem Stockwerk.

    floors() & areas() liefert einfach eine vollständige Liste.

    floor_id(), floor_name(), area_id(), area_name() liefern dann name und friedly_name.

    floor_areas(), floor_entities(), area_entities(), area_devices() liefern das was der Name der Funktion vermuten läßt.

    Als Parameter ist dabei so gut wie fast eh alles erlaubt.
    Hier noch ein kleines KI generiertes Beispiel dazu.

    HTML-Code:
    {%- for f_id in floors() -%}
    Floor|Stockwerk: {{ floor_name(f_id) }} (ID: {{ f_id }})
    {%- set areas = floor_areas(f_id) -%}
    {%- if areas | count > 0 %}
      Areas|Bereiche:
      {%- for a_id in areas %}
      - {{ area_name(a_id) }} (ID: {{ a_id }})
      {%- endfor %}
    {%- else %}
      (Keine Bereiche zugewiesen)
    {%- endif %}
    {% endfor -%}
    Zuletzt geändert von scw2wi; Gestern, 10:59.

    Kommentar


      #3
      Entities for an Integration

      integration_entities() liefert die Entities sowohl für eine Integration, als auch für eine Instanz der Integration.

      HTML-Code:
      {{ integration_entities('FRITZ!Box 7590') }}
      {{ integration_entities('Demo') }}

      Labels

      Liste aller Labels mit label_name(), label_description()

      HTML-Code:
      {% for label in labels() %}
      - ID: {{ label }}
        Name: {{ label_name(label) }}
        Beschreibung: {{ label_description(label) }}
      {% endfor %}
      label_areas(), label_devices(), label_entities() liefern Listen, wo so ein Label zugeordnet ist.


      Issues

      issues() Liefert alle non-hidden Reparatur-Meldungen mit domain & issue_id,
      issue(domain, issue_id) kann einzelne Meldungen abfragen.

      Den Level (warning, error) konnte ich nicht finden, auch keine Möglichkeit zur Abfrage von hidden Meldungen.
      Möglicherweise bietet Spook hier mehr Möglichkeiten.

      HTML-Code:
      {% set all_issues = issues() %}
      Anzahl: {{ all_issues.items() | list | count }}
      Details:
      {% for key, value in all_issues.items() -%}
        - Domain: {{ key[0] }} | ID: {{ key[1] }}
      {% endfor %}

      Immediate-if (iif)

      Es gibt in Jinja2 sowohl inline-if als auch immediate-if

      1x inline-if vs 2x iif
      HTML-Code:
      {{ 'Yes' if is_state('light.bed_light', 'on') else 'No' }}
      
      {{ iif(is_state('light.bed_light', 'on'), 'Yes', 'No') }}
      {{ is_state('light.bed_light', 'on') | iif('Yes', 'No') }}
      Obwohl laut Doku auch ein Parameter für if_none existiert konnte ich das weder mit 'None' noch mit 'none' erfolgreich testen.
      Zuletzt geändert von scw2wi; Gestern, 10:58.

      Kommentar


        #4
        Time

        Ich hab hier schon früher mal eine Zusammenfassung verschiedener Time-Funktionen erstellt.

        Unterscheiden muss man das datetime Objekt und verschiedene timestamp Formate.

        datetime im ISO-8601 Format gibt es mit T (das wird überall akzeptiert) und ohne T (macht manchmal Probleme).

        UNIX timestamp ist die Anzahl der Sekunden seit 1.1.1970

        now(), utcnow() und viele weitere Funktionen liefern alle das Format ohne T.

        isoformat() konvertiert vom Format ohne T in das Format mit T.

        timestamp_local(), timestamp_utc() liefern bereits das Format mit T.

        today_at('HH:MM:SS') konvertiert eine Uhrzeit in ein datetime Objekt des heutigen Datums.

        as_datetime(TIMESTAMP, 'None') konvertiert alle erkannten Timestamps in ein datetime Objekt oder liefert den Error-Wert None, wenn das nicht gelingt. Die Default-Zeitzone ist zwar UTC, kann aber mit as_local() nachträglich noch korrigiert werden.

        as_timestamp(TIMESTAMP_OR_DT, 'None') konvertiert alle erkannten Timestamps oder ein datetime Objekt in einen UNIX-Timestamp.

        timestamp_local('None') und timestamp_utc('None') konvertieren vom UNIX-Timestamp in das ISO-8601 Format mit T.

        Bei timestamp_custom() kann man das Format frei wählen und mit dem 2. Parameter zwischen UTC (false) und dem Default-Wert local (true) umschalten.

        Mit strptime() kann man jedes beliebige Format parsen, man muss es nur entsprechend angeben.

        Als Error-Returnvalue hab ich immer 'None' gewählt, das ist zwar manchmal der Default-Wert, aber leider nicht in allen Fällen, und dann wird die Verarbeitung einfach abgebrochen, und das wollen wir ja nicht.

        HTML-Code:
        {% set dt_local = now() %}
        {% set dt_utc = utcnow() %}
        
        {{ dt_local }}
        {{ dt_utc }}
        
        {{ dt_local.hour }}
        {{ dt_utc.hour }}
        
        {{ dt_local.isoformat(timespec='auto') }}
        {{ dt_local.isoformat(timespec='seconds') }}
        
        {{ today_at('12:00') }}
        
        {{ as_datetime('123456','None') }}
        {{ as_datetime('123456','None') | as_local() }}
        {{ as_datetime('2025-12-31 12:00','None') }}
        {{ as_datetime('2025-12-31 12:00','None') | as_local() }}
        
        {{ dt_local.timestamp() }}
        {{ dt_local | as_timestamp }}
        {{ as_timestamp('2025-12-31','None') }}
        
        {{ as_timestamp('2025-12-31','None') | timestamp_local('None') }}
        {{ as_timestamp('2025-12-31','None') | timestamp_utc('None') }}
        {{ as_timestamp('2025-12-31 12:00','None') | timestamp_custom('Am %d.%m.%y um %H Uhr UTC',false,'None') }}
        
        {{ strptime('2026-01-01 12:00','%Y-%m-%d %H:%M','None') }}
        {{ strptime('1.1.26 12:00','%d.%m.%y %H:%M','None') }}
        Mit timedelta() kann man eine Zeitdifferenz sehr übersichtlich definieren, as_timedelta() macht das gleiche, nur kompakter.

        relative_time() kann man durch time_since() oder time_until() ersetzen, oder gleich durch das wesentlich mächtigere Makro relative_time_plus().

        Mein ähnliches, jedoch einfacheres Makro relative_time_de() hab ich hier beschrieben.

        HTML-Code:
        {{ relative_time(now() - timedelta( hours = 1, minutes = 30 )) }}
        {{ relative_time(now() + timedelta( hours = 1, minutes = 30 )) }}
        
        {{ time_since(now() - timedelta( hours = 1, minutes = 30 ),2) }}
        {{ time_until(now() + timedelta( hours = 1, minutes = 30 ),2) }}
        
        {% from 'relative_time_plus.jinja' import relative_time_plus -%}
        {{ relative_time_plus(now() - timedelta( hours = 1, minutes = 30 ),2, language='de', abbr='true') }}
        {{ relative_time_plus(now() + timedelta( hours = 1, minutes = 30 ),2, language='de', abbr='true') }}
        
        {{ timedelta( weeks = 1, days = 1, hours = 1, minutes = 30 ) }}
        {{ as_timedelta('8 1:30:00') }}
        Zur Prüfung auf datetime (mit is datetime) siehe Kapitel Type-Checking etwas weiter unten.
        Zuletzt geändert von scw2wi; Gestern, 14:31.

        Kommentar


          #5
          To/From JSON

          Obwohl Python-Objekte und JSON-Strings auf den ersten Blick sehr ähnlich aussehen sind sie nicht dasselbe, können aber leicht von einem in das andere umgewandelt werden.

          Wenn wir also z.B. von MQTT oder einer Website einen JSON-String erhalten, dann müssen wir ihn in ein Objekt umwandelt, um es dann weiter verarbeiten zu können.

          Hier das Beispiel aus der Doku mit from_json() und to_json()

          HTML-Code:
          {% set json_string = '{"temperature": 25, "unit": "°C"}' %}
          {% set python_object = json_string | from_json('None') %}
          {{ python_object.temperature }}{{ python_object.unit }}
          
          {% set json_string_ascii = python_object | to_json(sort_keys=True, pretty_print=True, ensure_ascii=True) %}
          {{ json_string_ascii }}
          sort_keys sortiert die Keys, pretty_print sorgt für die Einrückungen, ensure_ascii wandelt Unicode in Escape Sequenzen um.


          is defined

          Bei HA funktioniert is defined oft nicht wie erwartet, da viele Funktionen im Fehlerfall 'None' zurück liefern, und None ist defined, also erhalten wir unerwartet oft True. Die Lösung ist, auch noch auf is not none zu prüfen.

          Hier ein Beispiel, wo is defined wie erwartet funktioniert.

          HTML-Code:
          {% set json_string = '{"temperature": 25, "unit": "°C"}' %}
          {% set python_object = json_string | from_json('None') %}
          {{ python_object.temperature is defined }}
          {{ python_object.unit is defined }}
          {{ python_object.xyz is defined }}

          Version

          version() erlaubt sichere Vergleiche der HA-Version (also minor 10 > minor 9) und den Zugriff auf major, minor und patch.

          Ich lasse mir damit am Dashboard anzeigen, sobald es eine neue Version gibt und der Patch-Level >= 2 ist.

          HTML-Code:
          {{ states('sensor.ha_version_installed') }}
          {{ states('sensor.ha_version_latest') }}
          
          {{ version(states('sensor.ha_version_installed')) }}
          {{ version(states('sensor.ha_version_latest')) }}
          
          {{ version(states('sensor.ha_version_latest')).major }}
          {{ version(states('sensor.ha_version_latest')).minor }}
          {{ version(states('sensor.ha_version_latest')).patch }}
          
          {{ version(states('sensor.ha_version_latest')) > version(states('sensor.ha_version_installed')) }}
          {{ version('2025.10.1') > version('2025.9.2') }}
          {{ '2025.10.1' > '2025.9.2' }}
          Zuletzt geändert von scw2wi; Gestern, 11:34.

          Kommentar


            #6
            Distance

            Device-Tracker sind mir immer ein wenig suspect, aber wer die Funktion enabled der kann sich mit distance() die Entfernung zum Home oder jedem anderen Punkt ausrechnen lassen oder auch das näheste Gerät finden.

            HTML-Code:
            {% set tracker = states.device_tracker
              | selectattr('attributes.latitude', 'defined')
              | list %}
            
            {% for item in tracker %}
            - Name: {{ item.attributes.friendly_name | default(item.entity_id) }}
              Entfernung: {{ distance(item) | round(0) }} km
            {% endfor %}
            closest() liefert aus einer Liste den nähesten, sofern Koordinaten vorhanden sind.

            HTML-Code:
            {{ closest(states.person).name }}
            Wer zusätzlich auch noch die Places Integration installiert, der kann sich sogar die Adresse (in verschiedenen Formaten) anzeigen lassen.

            HTML-Code:
            {% set closest_place = states.sensor
              | selectattr('entity_id', 'search', '^sensor\.places_')
              | sort(attribute='attributes.distance_from_home_m')
              | first %}
            
            {{ closest_place.attributes.formatted_address if closest_place else 'kein Places-Sensor gefunden' }}
            Die closest() Funktion geht bei dieser Integration aber nicht, da die Attribute für Koordinaten hier anders genannt wurden (current_latitude statt nur latitude).

            In der Doku gibt es noch viele weitere Beispiele, auch mit groups.


            Contains

            Der contains Filter funktioniert so ähnlich wie der in-Operator, hat aber teilweise Vorteile, wie etwa im folgenden Beispiel wo er gleich im selectattr angewendet wird, und daher eine weitere if-Abfrage einspart.

            HTML-Code:
            {% set lights = states.light
              | selectattr('attributes.supported_color_modes', 'defined')
              | selectattr('attributes.supported_color_modes', 'contains', 'color_temp')
              | list %}
            
            {% for light in lights %}
            - Name: {{ light.attributes.friendly_name }}
              Min Mireds: {{ light.attributes.min_mireds | default('N/A') }}
              Max Mireds: {{ light.attributes.max_mireds | default('N/A') }}
            {% endfor %}
            Wenn man das ohne den contains-Filter lösen möchte, dann muss man in der Schleife noch eine If-Abfrage einbauen und dort erst filtern.

            HTML-Code:
            {% for light in lights %}
              {% if 'color_temp' in light.attributes.supported_color_modes %}
              ...
            Zuletzt geändert von scw2wi; Gestern, 14:50.

            Kommentar


              #7
              Numeric Functions and Filters

              Die meisten Funktionen kann man auch als Filter verwenden, wie die ersten 3 Zeilen zeigen.

              Ein Default-Wert (z.B. 'None') sollte übrigens immer mit angegeben werden, wenn man Errors vermeiden möchte.

              Der Filter "| is_number" (Achtung: mit _) prüft, ob ein String in eine Zahl umgewandelt werden könnte.

              Der Test "is number" (Achtung: ohne _) prüft, ob ein Wert bereits eine Zahl ist.

              Siehe dazu auch: typeof() im Kapitel Determining Types

              HTML-Code:
              {{ float('not_a_number', default='None') }}
              {{ 'not_a_number' | float(default='None') }}
              {{ 'not_a_number' | float('None') }}
              
              {{ 'not_a_number' | is_number }}
              {{ '12.3' | is_number }}
              {{ '12.3' is number }}
              {{ '12.3' | float('None') is number }}
              
              {{ '12.3' | float('None') is integer }}
              {{ '12.3' | float('None') is float }}
              {{ '12.3' | int('None') is integer }}
              {{ '12.3' | int('None') is float }}
              
              {{ 'true' | bool('None') }}
              {{ 'yes' | bool('None') }}
              {{ 'on' | bool('None') }}
              {{ '1' | bool('None') }}
              {{ 'enable' | bool('None') }}
              {{ 'enabled' | bool('None') }}
              
              {{ 'false' | bool('None') }}
              {{ 'no' | bool('None') }}
              {{ 'off' | bool('None') }}
              {{ '0' | bool('None') }}
              {{ 'disable' | bool('None') }}
              {{ 'disabled' | bool('None') }}
              HTML-Code:
              {{ log( 1000, 10, default='None' ) }}
              {{ log( 20, e, default='None' ) }}
              {{ sqrt( 25, default='None' ) }}
              
              {{ sin( 30 * (pi / 180), default='None' ) }}
              {{ cos( 60 * (pi / 180), default='None' ) }}
              {{ tan( 45 * (pi / 180), default='None' ) }}
              
              {{ asin( 0.5, default='None' ) * (180 / pi) }}
              {{ acos( 0.5, default='None' ) * (180 / pi) }}
              {{ atan( 1.0, default='None' ) * (180 / pi) }}
              HTML-Code:
              {{ min( [10, 100, 1000] ) }}
              {{ max( [10, 100, 1000] ) }}
              
              average: {{ average( [10, 100, 1000], default='None' ) }}
              median: {{ median( [10, 100, 1000], default='None' ) }}
              erster häufigster Wert: {{ statistical_mode( [100, 10, 100, 50, 10, 100, 10], default='None' ) }}
              clamp() begrenzt, wrap() bricht um, remap() transformiert und kann auch wrappen

              HTML-Code:
              clamp(v, min, max): {{ clamp( 10, 200, 1000 ) }}
              wrap(v, min, max): {{ wrap( 390, 0, 360 ) }}
              
              remap(v, in_min, in_max, out_min, out_max, *, [steps], [edges]):
              von 100 auf 255: {{ remap( 99, 0, 100, 0, 255, steps=255 ) }}
              von 255 auf 100: {{ remap( 253, 0, 255, 0, 100, steps=100 ) }}
              von rad nach Grad mit wrap: {{ remap( 3.14, 0, 2*pi, 0, 360, steps=360, edges='wrap' ) }}
              round() mit Standard-Verhalten, floor, ceil und (bei precision=1) half, das rundet dann z.B. auf 20.0, 20.5, 21.0 was bei der Temperatur gerne gemacht wird.

              HTML-Code:
              {{ '20.416' | round( 2, default='None' ) }}
              {{ '20.416' | round( 0, 'floor', default='None' ) }}
              {{ '20.416' | round( 0, 'ceil', default='None' ) }}
              {{ '20.416' | round( 1, 'half', default='None' ) }}
              bitwise_and, or, xor

              HTML-Code:
              {{ '{:04b}'.format(('1010' | int(base=2)) | bitwise_and (0b0011)) }}
              {{ '{:04b}'.format(('1010' | int(base=2)) | bitwise_or  (0b0011)) }}
              {{ '{:04b}'.format(('1010' | int(base=2)) | bitwise_xor (0b0011)) }}
              Die Ausgabe erfolgt hier als 4-stellige Binärzahl mit: '{:04b}'.format(value)

              Der Filter ord kann ein einzelnes char in die Unicode-Zahl umwandeln.

              Die Gegenrichtung ist aber etwas komplizierter, da es chr() weder als Funktion noch als Filter gibt.

              HTML-Code:
              {{ ('A') | ord  }}
              {{ ('Ä') | ord  }}
              {{ ('€') | ord  }}
              {{ ('😊') | ord  }}
              {{ "%c" | format(65) }}
              {{ "%c" | format(128522) }}
              Die Filter multiply und add sind bei Listen in Verbindung mit map() vorteilhaft.

              HTML-Code:
              {{ [1, 2, 3] | map('multiply', 2) | list }}
              {{ [1, 2, 3] | map('add', 2) | list }}
              Zuletzt geändert von scw2wi; Gestern, 11:57.

              Kommentar


                #8
                Complex Type-Checking & Type-Conversions

                is defined haben wir oben schon kennengelernt, is_number hab ich auch bereits kurz erwähnt, hier nun weitere checks.

                List ist eine änderbare Liste [] mit definierter Reihenfolge, wo auch Duplikate möglich sind.

                Set ist eine änderbare Menge ohne definierte Reihenfolge und ohne Duplikate. In Jinja2 werden sie mit der Funktion set() aus einer Liste erstellt.

                Tuple ist eine speicherschonende Liste, die dynamisch nicht geändert werden kann. Sie kann entweder mit {} direkt oder über die Funktion tuple() aus einem Set erstellt werden.

                Dictionaries sind key/value Paare: { 'key': 'value' } und liefern bei "is mapping" True.

                HTML-Code:
                {{ [1, 2, 4, 2] is list }}
                {{ set([1, 2]) is set }}
                {{ ('a', 'b', 'c') is tuple }}
                {{ tuple('abc') is tuple }}
                {{ {'a': 1, 'b': 2, 'c': 3} is mapping }}
                datetime und Bool-Objekte kennen wir bereits.

                string_like klingt jetzt etwas seltsam, prüft aber nicht nur auf Strings sondern auch auf Byte-Arrays, wie sie zwar in Python üblich sind, weniger jedoch in Jinja2.

                HTML-Code:
                {{ now() is datetime }}
                {{ 'enable' | bool('None') is boolean }}
                {{ 'enabled' | bool('None') is boolean }}
                {{ 'abc' is string_like }}
                {{ 'abc' is string }}
                Bei der Prüfung auf is callable müssen wir immer auch auf is defined prüfen, da auch alles was nicht defined ist, als callable leider True meldet.

                HTML-Code:
                {% macro begrüßung() %}
                  Hallo Welt!
                {% endmacro %}
                {% set variable = "Hallo Welt!" %}
                
                {{ begrüßung is defined and begrüßung is callable }}
                {{ variable is defined and variable is callable  }}
                is integer und is float hatten wir oben bereits.

                HTML-Code:
                {{ '12.3' | float('None') is integer }}
                {{ '12.3' | float('None') is float }}
                {{ '12.3' | int('None') is integer }}
                {{ '12.3' | int('None') is float }}
                is iterable liefert alles was iterierbar ist, also list, set, tuple, dictionary und string.

                HTML-Code:
                {{ [1, 2, 4, 2] is iterable }}
                {{ set([1, 2]) is iterable }}
                {{ tuple('abc') is iterable }}
                {{ {'a': 1, 'b': 2, 'c': 3} is iterable }}
                {{ 'abc' is iterable }}
                Bei der Prüfung auf is sequence steigt das set aus, da es nicht indexierbar ist. Wir können also nicht das 2. Element aus einer Menge holen.

                HTML-Code:
                {{ [1, 2, 4, 2] is sequence }}
                {{ set([1, 2]) is sequence }}
                {{ tuple('abc') is sequence }}
                {{ {'a': 1, 'b': 2, 'c': 3} is sequence }}
                {{ 'abc' is sequence }}
                Zuletzt geändert von scw2wi; Gestern, 12:14.

                Kommentar


                  #9
                  Der Check auf is none darf in dieser Liste nicht fehlen, daher hab ich ihn hier eingefügt. In der Doku kommt er im Kapitel "States" bei den Attributen kurz vor.

                  Zuerst wollen wir den check auf none bei einem Sensor-State testen, um gleich feststellen zu müssen, dass so etwas gar nicht möglich ist. Für den Test holen wir uns zuerst mal eine Entity die tatächlich 'None' ist. Und da es bei None leider immer wieder Verwirrung gibt, prüfen wir auch gleich auf 'none'.

                  HTML-Code:
                  {% set none_entities = states
                    | selectattr('state', 'in', ['None','none','unavailable','unknown'])
                    | map(attribute='entity_id')
                    | sort
                    | list
                  %}
                  
                  Es gibt {{ none_entities | length }} Entitäten im Status 'None' oder 'none':
                  
                  {%- for entity in none_entities %}
                    {{- '\n' -}} - {{ entity }} ({{ states(entity) }})
                  {%- endfor %}
                  Bei mir kommen solche Entities wesentlich seltener vor als unavailable oder unknown, auf die hier auch gleich geprüft wurde. Warum das so ist, wird jetzt gleich klar werden, denn None ist in vielen Fällen eigentlich falsch angewendet, da es sich um einen gültigen Wert handelt. Den folgenden Code muss jeder selbst editieren, falls oben None oder none Werte gefunden wurden.

                  HTML-Code:
                  {{ states('sensor.WITH_STATE_None') }}
                  {{ states('sensor.WITH_STATE_none') }}
                  
                  {{ states('sensor.WITH_STATE_None') is none }}
                  {{ states('sensor.WITH_STATE_none') is none }}
                  
                  {{ states('sensor.WITH_STATE_None') in ['none','None'] }}
                  {{ states('sensor.WITH_STATE_none') in ['none','None'] }}
                  
                  {{ has_value('sensor.WITH_STATE_None') }}
                  {{ has_value('sensor.WITH_STATE_none') }}
                  Die Enttäuschung ist recht groß, aber sowohl None als auch none liefern bei Prüfung auf is none False zurück, beides sind daher gültige Werte, was uns auch has_value bestätigt.

                  Als nächstes machen wir ein paar Checks, die wie erwartet funktionieren.
                  Die Prüfung auf is none bei einem Attribut das bei mir null ist, und has_value auf Sensoren die unavailable oder unknown sind.

                  HTML-Code:
                  {{ state_attr('update.auto_entities_update','title') }}
                  {{ state_attr('update.auto_entities_update','title') is none }}
                  
                  {{ has_value('sensor.WITH_STATE_unavailable') }}
                  {{ has_value('sensor.WITH_STATE_unknown') }}
                  Wir merken uns, Prüfung auf is none funktioniert bei Attributen (denn die können null werden), nicht aber beim State.
                  has_value liefert nur dann False, wenn der State in ['unavailable','unknown'] ist, nicht jedoch bei None oder none.

                  Zwischenfrage: Hat sich schon mal jemand darüber gewundert, dass wir den State mit states() abfragen, und nicht mit state()? Ich hab da jedenfalls noch nie mehr als einen Wert zurück bekommen.
                  Zuletzt geändert von scw2wi; Gestern, 12:27.

                  Kommentar


                    #10
                    Iterating multiple Objects

                    Es gibt in Jinja2 tatsächlich eine zip() Funktion, sie funktioniert aber etwas anders als unter Windows.

                    Als Beispiel nehmen wir 3 Listen her, die wir synchron durchlaufen wollen, z.B. weil wir Key/Value1/Value2 getrennt übertragen bekommen.

                    HTML-Code:
                    {%- set sensoren = ['temp', 'humidity', 'pressure'] -%}
                    {%- set werte = [22, 55, 1013] -%}
                    {%- set einheiten = ['°C', '%', ' mbar'] -%}
                    Gemergte Listen:
                    {%- for sensor, value, unit in zip(sensoren, werte, einheiten) %}
                      {{ sensor }}: {{ value }}{{ unit }}
                    {%- endfor %}

                    Functions and Filters to process raw data

                    Nach zip() kommen jetzt die Funktionen/Filter pack() und unpack() dran, um native Python Typen in Byte Objekte und zurück zu konvertieren. Wenn man z.B. über Modbus RAW-Daten erhält, dann kann man das damit in eine lesbare Form umwandeln.

                    Hier ein Beispiel aus der Doku und zwei von der KI (mit 4.0 als float und double)

                    HTML-Code:
                    {{ "0x%X" % 0xDEADBEEF | pack(">I") | unpack(">I") }}
                    {{ 0x40800000 | pack(">I") | unpack(">f") | float('None') }}
                    {{ 0x4010000000000000 | pack(">Q") | unpack(">d") | float('None') }}
                    0x%X formatiert die Ausgabe wieder als HEX-String

                    Der Parameter für pack und unpack erwartet ein > Zeichen für Big-Endian, und ein < Zeichen für Little-Endian,
                    weiters ein b/B für signed/unsigned Byte, h/H für 16-bit Short, i/I für 32-bit Int, f für 4 Byte Float und d für 8 Byte Double.

                    Sinnvoll einsetzen kann man das aber eher nur in Phyton direkt, da Jinja2 nicht so gut mit RAW-Daten umgehen kann.


                    String Filters

                    urlencode liefert die Kodierung für URls

                    slugify liefert den HA name zu einem friendly_name

                    base64_encode und _decode machen das, was der Name sagt. base64_decode(None) liefert RAW-Bytes
                    from_hex wandelt einen HEX-String in RAW-Bytes.

                    HTML-Code:
                    {% set text = "Text mit Leerzeichen und ä-ö-ü-ß" %}
                    {{ text | urlencode }}
                    {{ text | slugify }}
                    
                    {{ text | base64_encode }}
                    {{ 'VGV4dCBtaXQgTGVlcnplaWNoZW4gdW5kIMOkLcO2LcO8LcOf' | base64_decode }}
                    {{ 'VGV4dCBtaXQgTGVlcnplaWNoZW4gdW5kIMOkLcO2LcO8LcOf' | base64_decode(None) }}
                    {{ 'DEADBEEF' | from_hex }}
                    ordinal liefert: 1st, 2nd, 3rd, 4th, usw.

                    HTML-Code:
                    {{ 1 | ordinal }}
                    {{ 2 | ordinal }}
                    {{ 3 | ordinal }}
                    {{ 4 | ordinal }}

                    Hashing

                    Es gibt folgende Funktionen/Filter: md5, sha1, sha256, sha512. Beispiele dazu siehe Doku.
                    Zuletzt geändert von scw2wi; Gestern, 12:42.

                    Kommentar


                      #11
                      Regular Expressions

                      Mit is match und is search kann man am Anfang oder im gesamten String suchen, mit regex_replace() kann man ersetzen.

                      HTML-Code:
                      {% set text = "Text mit Leerzeichen und ä-ö-ü-ß" %}
                      {{ text is match('Text mit', ignorecase=False) }}
                      {{ text is match('Leerzeichen', ignorecase=False) }}
                      {{ text is search('Leerzeichen', ignorecase=False) }}
                      {{ text | regex_replace(find='leerzeichen', replace='Blank', ignorecase=True) }}
                      Hier wird nach dem Text "licht oder light gesucht, wobei flight ausgeschlossen wird.

                      HTML-Code:
                      {%- set entities = states.sensor | list + states.light | list -%}
                      {%- set light_entities = entities
                        | selectattr('name', 'search', '(?<!f)light|licht', ignorecase=True)
                        | list %}
                      {%- for e in light_entities %}
                        {{ e.domain }}: {{ e.attributes.friendly_name | default(e.entity_id) }}
                      {%- endfor %}
                      regex_findall sucht hier nach allen Zahlen und liefert ein Array von Treffern

                      regex_findall_index liefert nur einen dieser Treffer und zwar laut Index (0 ist der erste, 1 der zweite, ...)

                      regex_replace ersetzt hier Kommapunkte durch Kommabeistriche

                      HTML-Code:
                      {% set text = "Temperatur: 22.5°C, Feuchtigkeit: 45%" %}
                      {{ text | regex_findall(find='\d+\.?\d*') }}
                      {{ text | regex_findall_index(find='\d+\.?\d*', index=0) }}
                      {{ text | regex_replace(find='(\d)\.(\d)', replace='\\1,\\2') }}
                      Zuletzt geändert von scw2wi; Gestern, 12:48.

                      Kommentar


                        #12
                        Shuffling

                        Mit shuffle kann man eine Liste neu mischen. Das Default-Verhalten ist zufällig aber mit einem Seed kann man es reproduzierbar gestalten. Ich habe als Seed das aktuelle Datum gewählt, damit kann man z.B. eine Playlist jeden Tag neu mischen.

                        HTML-Code:
                        {% set list = [1, 2, 3, 4, 5, 6, 7, 8, 9] %}
                        
                        {{ list | shuffle }}
                        {{ list | shuffle }}
                        {{ list | shuffle }}
                        
                        {{ list | shuffle(now().strftime('%Y-%m-%d')) }}
                        {{ list | shuffle(now().strftime('%Y-%m-%d')) }}
                        {{ list | shuffle(now().strftime('%Y-%m-%d')) }}

                        Flatten a List of Lists

                        Mit flatten() kann man verschachtelte Listen in eine flache Liste umwandeln, mit levels kann man auch angeben, bis zu welcher Ebene das erfolgen soll. Bei diesem Beispiel gibt es aber nur eine Ebene.

                        HTML-Code:
                        {% set nested_list = areas()
                          | map('area_entities')
                          | reject('equalto', [])
                          | list %}
                        Verschachtelte Liste:
                        {{ nested_list }}
                        
                        {% set flat_list = nested_list | flatten(levels=1) %}
                        Flache Liste:
                        {{ flat_list }}

                        Combining Dictionaries

                        combine() ist das Gegenstück zu flatten(), hier werden Dictionaries vereint.

                        Das Default-Verhalten ist aber genau umgekehrt, verschachtelte Dictionaries werden nur dann aufgelöst, wenn recursive=True gesetzt wird. Verwendet wird das gerne, um Default-Settings zu overrulen.

                        HTML-Code:
                        {% set default_config = {'helligkeit': 100, 'mired': '300'} %}
                        {% set user_config = {'helligkeit': 50} %}
                        ohne recursive: {{ default_config | combine(user_config) }}
                        mit recursive:  {{ default_config | combine(user_config, recursive=True) }}
                        
                        {%- set default_config2 = {'licht': {'helligkeit': 100, 'mired': '300'}} %}
                        {% set user_config2 = {'licht': {'helligkeit': 50}} %}
                        ohne recursive: {{ default_config2 | combine(user_config2) }}
                        mit recursive:  {{ default_config2 | combine(user_config2, recursive=True) }}
                        Zuletzt geändert von scw2wi; Gestern, 12:54.

                        Kommentar


                          #13
                          Find common Elements between Lists, in 1. but not in 2., either but not in both, combine all

                          Mit intersect(LIST1, LIST2) liefert die Schnittmenge von 2 Listen (A ∩ B), also alle Elemente die in beiden vorkommen.

                          Mit union(LIST1, LIST2) liefert die Vereinigungsmenge von 2 Listen (A ∪ B)

                          Mit difference(LIST1, LIST2) liefert die Restmenge (A \ B), also alle Elemente von Liste1 die in Liste2 nicht vorkommen.

                          symmetric_difference kann man mit XOR vergleichen, also entweder in 1 oder in 2 aber nicht in beiden. (A ∆ B)

                          HTML-Code:
                          {% set primzahlen = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29] %}
                          {% set fibonacci = [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] %}
                          
                          Primzahlen: {{ primzahlen }}
                          Fibonacci: {{ fibonacci }}
                          
                          Intersect: {{ primzahlen | intersect(fibonacci) | sort  }}
                          Union: {{ primzahlen | union(fibonacci) | sort }}
                          
                          Difference 1-2: {{ primzahlen | difference(fibonacci) | sort }}
                          Difference 2-1: {{ fibonacci | difference(primzahlen) | sort }}
                          
                          Sym-Difference: {{ primzahlen | symmetric_difference(fibonacci) | sort }}

                          Working with Macros

                          Mit apply kann man ein Makro oder eine Funktion als Filter auf Elemente einer Liste anwenden.

                          Im ersten Beispiel wenden wir das Makro nur auf eine einzelne Entity an, dafür hätten wir apply aber nicht benötigt.
                          Das zweite Beispiel zeigt dann, was apply im Zusammenhang mit map leistet. Wir hätten dafür als Alternative eine Schleife benötigt.

                          HTML-Code:
                          {% macro format_light(light_obj) %}
                            {{ light_obj.name }} ist {{ 'hell' if light_obj.state == 'on' else 'dunkel' }}
                          {% endmacro %}
                          
                          {# Wendet das Makro auf eine Entität an, das ginge auch noch direkt #}
                          {{ states.light.bed_light | apply(format_light) }}
                          {{ format_light(states.light.bed_light) }}
                          
                          {%- set entities = states.light | list -%}
                          {%- set light_entities = entities
                            | selectattr('name', 'search', '(?<!f)light|licht', ignorecase=True)
                            | list %}
                          
                          {{ light_entities | map('apply', format_light) | map('trim') | join('\n') }}
                          Zuletzt geändert von scw2wi; Gestern, 12:59.

                          Kommentar


                            #14
                            Working with Macros​ - Teil 2

                            Makros geben als Return-Value üblicherweise nur einen String zurück.
                            Mit as_function kann man ein Makro dazu bringen, jeden beliebigen Datentyp zu liefern, so wie eine Funktion das auch kann.

                            Es war gar nicht so einfach, der KI ein lauffähiges und praxisgerechtes Beispiel zu entlocken, und selbst hätte ich das ohnehin nie geschafft, aber dieses Makro gibt gleich ein komplettes Objekt zurück.

                            HTML-Code:
                            {% macro wetter_info(state, temp, returns) -%}
                              {%- set temp = temp | float(default='None') -%}
                              {%- set mapping = {
                                'sunny': {'text': 'Sonnig', 'icon': '☀️'},
                                'cloudy': {'text': 'Bewölkt', 'icon': '☁️'},
                                'rainy': {'text': 'Regnerisch', 'icon': '🌧️'},
                                'clear-night': {'text': 'Klare Nacht', 'icon': '🌙'}
                              } -%}
                              {# Standardwerte falls Zustand unbekannt #}
                              {%- set info = mapping.get(state, {'text': 'Unbekannt', 'icon': '❓'}) -%}
                              {%- set temp_display = "-?-" -%}
                              {%- set empfehlung = "Traue nicht diesem Wetterbericht" -%}
                              {%- set frost = false -%}
                              {# Nur ausführen, wenn temp wirklich eine Zahl ist #}
                              {%- if temp is number -%}
                                {%- set empfehlung = 'Jacke mitnehmen' if temp < 15 else 'T-Shirt Wetter' -%}
                                {%- set frost = temp < 0 -%}
                                {%- set temp_display = temp | round(1) ~ "°C" -%}
                              {%- endif -%}
                              {# Rückgabe als echtes Objekt #}
                              {%- do returns({
                                'anzeige': info.icon ~ " " ~ info.text,
                                'temperatur': temp_display,
                                'tipp': empfehlung,
                                'frostgefahr': frost
                              }) -%}
                            {%- endmacro %}
                            
                            {%- set wetter_checker = wetter_info | as_function %}
                            {# Beispiel Abfrage über forecast_home, bitte je nach Wetter-Integration entsprechend anpassen #}
                            {%- set aktuelles_wetter = wetter_checker(states('weather.forecast_home'), state_attr('weather.forecast_home', 'temperature')) %}
                            Wetterbericht:
                            Status: {{ aktuelles_wetter.anzeige }}
                            Temperatur: {{ aktuelles_wetter.temperatur }}
                            Empfehlung: {{ aktuelles_wetter.tipp }}
                            
                            {%- if aktuelles_wetter.frostgefahr %}
                            ⚠️ Achtung, es gibt Frost!
                            {% endif %}
                            Zuletzt geändert von scw2wi; Gestern, 14:24.

                            Kommentar


                              #15
                              Merge Action-Responses

                              merge_response() ist ein Template mit dem Action-Responses zusammengefügt werden können.
                              Achtung, diese dürfen keinen Key "entity_id" beinhalten.

                              Die beiden Keys entity_id (für die Quelle) und value_key (für das Quell-Service) werden hinzugefügt.

                              Es gibt in der Doku ein sehr schönes Beispiel zum mergen von 2 Kalendern, daher spare ich mir das hier.


                              Processing incoming Data

                              Dieses Kapitel beschreibt sehr ausführlich die Anwendung von value_template Keys anhand von value_json und MQTT.
                              Da auch hier die Beispiele umfangreich genug sind, gibt es von mir wieder nichts hinzuzufügen.


                              Determining Types

                              Mit typeof (als Funktion oder Filter) können wir auf viele Typen prüfen: int, float, str, list, dict, bool, NoneType

                              siehe dazu auch das Kapitel Complex Type-Checking mit: is integer, is float, is string, is list, is tuple, is boolean, is none

                              Auch hier sind die Beispiele in der Doku ausreichend.


                              Some more things to keep in mind

                              Es kommt vor, dass die entity_id mit einer Ziffer beginnt.

                              Ein Beispiel bei mir ist der Kalender, der nach jeder neuen Instanz die Farben alphabetisch neu mischt.
                              Daher benenne ich die Kalender-Instanzen: 1_Thema1, 2_Thema2 und damit beginnen sie mit einer Ziffer.
                              Ich hätte sie auch a_Thema1, b_Thema2 nennen können, das wäre vermutlich besser gewesen.

                              Der folgende Code listet solche Entities auf.

                              HTML-Code:
                              {% set entities = states
                                | selectattr('entity_id', 'search', '^\w+\.\d')
                                | map(attribute='entity_id')
                                | sort
                                | list  %}
                              
                              {% for id in entities %}
                              - {{ id }}
                              {%- endfor %}
                              Mit states.domain.entity_id erhalten wir in so einem Fall nur: TemplateSyntaxError: expected token 'end of print statement', got 'integer'

                              Mit states.domain['entity_id'] erhalten wir das gewünschte Objekt.


                              Priority of Operators

                              Der Filter Operator hat Vorrang vor allen anderen Operatoren, daher sollte vor jedem Filter entweder ein anderer Filter, ein Ausdruck oder eine Klammer stehen, jedenfalls aber keine Rechenoperation, außer man will es so.

                              Im folgenden Fall möchte ich jedenfalls nicht zuerst die Zahl 1000 auf 2 Nachkommastellen runden und dann erst die Division durchführen.

                              HTML-Code:
                              {% set power = '1234.5' %}
                              {{ power | float / 1000 | round(2) }}
                              {{ (power | float / 1000) | round(2) }}
                              So, das war's. Ich hab das eigentlich alles nur für mich zur Übung erstellt, aber wenn's noch jemand anderem hilft, dann umso besser.

                              Wer nach all diesen Möglichkeiten jetzt immer noch der Meinung ist, HA würde ihn irgendwie einschränken, so wie das einige hier gerne behaupten, dem ist dann aber auch nicht mehr zu helfen.

                              Und sollte Jinja wirklich mal an seine Grenzen stoßen, dann kann man immer noch auf Python Scripts​ ausweichen.

                              Bei der Gelegenheit gleich: Happy New Year to all of you!

                              Hier geht's zum nächsten Beitrag: (coming soon)​
                              ​​​​hier zur HA Kurzeinführung​​​
                              und hier zur Liste aller Beiträge dieser Serie​​​​​
                              Zuletzt geändert von scw2wi; Gestern, 15:21.

                              Kommentar

                              Lädt...
                              X