Home Assistant Daten als Tabelle anzeigen / ausgeben

Charts sind super, aber für bestimmte Auswertungen wäre eine einfache Tabellenansicht historischer Daten wünschenswert. Bedauerlicherweise hat Home Assistant dazu, out of the box, sehr wenig zu bieten.

Nur über Umwege ist es möglich beliebige SQL-Abfragen der Datenbank in einer Lovelance-Card als Tabelle auszugeben, hier das Ziel dieses Beitrags: 

Das hier präsentierte Beispiel umfasst Daten von fünf verschiedenen Entitäten, die aus der Datenbank ausgelesen, in einer Entität gespeichert und über eine Markdown-Card in Lovelace ausgegeben werden. In der Markdown-Card können bestimmte Spalten auf Basis der Werte von anderen Spalten berechnet werden: z. B. wird die Spalte: "Delta" aus "Vorlauf" und "Rücklauf" berechnet. Für dieses Beispiel habe ich die Custom-Integration sql_json verwendet. Aber auch ohne der HACS-Integration sql_json können bestimmte Datenbank-Werte in einer Lovelance-Card angezeigt werden:

☑ Was in HA mit der Standard - SQL-Integration möglich ist (Empfohlen für kleine Datenmengen)

Mithilfe der vorhandenen SQL-Integration ist es möglich, das Ergebnis für bestimmte SQL-Abfragen in einem eigenen Sensor zu speichern. Leider ist die SQL-Integration für das Speichern nur eines Wertes einer Entität ausgelegt. Zudem haben die Sensoren an dieser Stelle ein Limit auf 255 Zeichen. Für kleine Datenmengen können dennoch mehrere Werte im csv-Format in einer Entität abgelegt werden, siehe: Home Assistant SQL - Integration. CSV-Daten können dann über eine Markdown-Card als Tabelle ausgegeben werden. Hier ein konkretes Beispiel für den täglichen PV-Ertrag: SQL-Integration Sensor, siehe: 

Nachfolgend die verwendete SQL-Query für eine fortlaufende Entität (Energiezähler) aus der States-Tabelle:

[+]
SELECT 
    GROUP_CONCAT(localdate || ":" || state, ",") AS val, 
    localdate 
FROM (
    SELECT 
        ROUND(Max(state) - Min(state) , 2) AS state,
        DATE(datetime(last_updated_ts, 'unixepoch', 'localtime')) AS localdate 
    FROM 
        states 
    WHERE 
        metadata_id = (
            SELECT metadata_id 
            FROM states_meta 
            WHERE entity_id = 'sensor.pv_panels_energy'
        ) 
        AND state NOT IN ("unknown", "", "unavailable") 
    GROUP BY 
        localdate 
    ORDER BY 
        localdate DESC
)

Der Sensor: sensor.pv_panels_energy muss natürlich entsprechend angepasst werden. Damit die Abfrage funktioniert, sollte diese vorab getestet werden, siehe: 3 Varianten: SQLite Datenbank Zugriff - Home Assistant.

Die Query liefert folgende Werte:

2025-02-18:29.03,2025-02-17:16.4,2025-02-16:10.88,2025-02-15:24.32,2025-02-14:5.72,2025-02-13:24.19,2025-02-12:20.57,2025-02-11:24.02,2025-02-10:22.22,2025-02-09:29.59,2025-02-08:41.39

Diese Werte können dann in einer Markdown-Card als Tabelle angezeigt werden:

Markdown:

[+]
{% set data = states('sensor.pv_energy_daily').split(",")  %}
<table><tr>
  <th>date</th>
  <th>value</th>
</tr>
{% for i in range(0,data | count) %}
 <tr>
   <td  width=100>
     {{data[i].split(":")[0]  }}
   </td>
   <td align=right>
     {{ data[i].split(":")[1]  }} kWh
   </td>
 </tr>
{% endfor %}

🙋Lösung für mehr als 255 Zeichen: sql_json (Empfohlen für große Datenmengen)

Die Limitation auf 255 Zeichen gilt nicht für "state_attributes" eines Datenbank-Eintrags, wodurch auch längere Einträge erstellt werden können. Voraussetzung ist eine HACS Integration, um Datenbankabfragen als JSON speichern zu können:

Mehr Daten als die States Tabelle liefert die History Tabelle. Abgelegt als Json in der Configuration.yaml speichert die folgende Query die Daten des Sensors "sensor.pv_panesl_energy" der letzten 100 Tage:

[+]
sensor:
  - platform: sql_json
    scan_interval: 86400
    queries:
      - name: "daily_pv_yield"
        query: >-
          SELECT json_group_array(
                    json_object(
                        'localdate', localdate, 
                        'state', state_diff
                    )
                ) AS json
          FROM (
              SELECT 
                  ROUND(MAX(state) - MIN(state), 2) AS state_diff,
                  DATE(datetime(created_ts, 'unixepoch', 'localtime')) AS localdate 
              FROM 
                  statistics 
              WHERE 
                  metadata_id = (
                      SELECT id 
                      FROM statistics_meta 
                      WHERE statistic_id = 'sensor.pv_panels_energy'
                  ) 
                  AND state NOT IN ("unknown", "", "unavailable") 
              GROUP BY 
                  localdate 
              ORDER BY 
                  localdate DESC
              LIMIT 100
          );
        value_template: '{{ value_json[0].state }}'
        column: json

Damit das Ergebnis der Abfrage nicht allzu oft zusätzlich in der Datenbank abgelegt wird, sollte der "scan_interval" nicht allzu niedrig angesetzt werden. Um dennoch aktuelle Daten zu bekommen, kann die Abfrage auch über die GUI angestoßen werden:

Werte aktualisieren

Die Query kann bei Bedarf auch über ein Skript oder eine Automatisierung ausgeführt werden:

Anzeige der Daten in einer Markdown-Karte

Für die Anzeige der Daten in Tabellenform kann am einfachsten eine Markdown-Karte verwendet werden:

{% set data = state_attr('sensor.daily_pv_yield','json')  %}
<table><tr>
  <th>Date</th>
  <th align=right>value</th>
</tr>
{% for i in range(0,data | count)%}
 <tr>
   <td align=right>
     {{data[i].localdate  }}
   </td>
   <td align=right width=100>
     {{ '{:.2f}'.format(data[i].state | round(2))  }} 
   </td>
 </tr>
{% endfor %}

Bestimmte Entitäten in einer Query kombinieren

Mit dieser Variante können die Daten mehrerer Sensoren kombiniert werden, hier die zugehörige Query zu dem anfangs in diesem Artikel präsentierten Beispiel:

[+]
      - name: "daily_test"
        query: >-
          SELECT  json_group_array(
                  json_object(
                      'localdate', flowmeter_sum.localdate, 
                      'flowmeter_sum', flowmeter_sum.state_diff,
                      'aussen_temperatur_mean', aussen_temperature.state_mean,
                      'flowmeter', flowmeter.state,
                      'heating_vorlauf', heating_vorlauf.state_min,
                      'heating_ruecklauf', heating_ruecklauf.state_min
                  )
              ) AS json
          FROM (
              SELECT 
                  ROUND(MAX(state) - MIN(state), 2) AS state_diff,
                  DATE(datetime(created_ts, 'unixepoch', 'localtime')) AS localdate 
              FROM 
                  statistics 
              WHERE 
                  metadata_id = (
                      SELECT id 
                      FROM statistics_meta 
                      WHERE statistic_id = 'sensor.flowmeter_sum'
                  ) 
                  AND state NOT IN ("unknown", "", "unavailable")                                      
                  AND DATE(datetime(created_ts, 'unixepoch', 'localtime')) < DATE('now')
              GROUP BY 
                  localdate 
              ORDER BY localdate DESC
          ) flowmeter_sum
          LEFT JOIN (
              SELECT 
                  ROUND(AVG(mean), 2) AS state_mean,
                  DATE(datetime(created_ts, 'unixepoch', 'localtime')) AS localdate 
              FROM 
                  statistics 
              WHERE 
                  metadata_id = (
                      SELECT id 
                      FROM statistics_meta 
                      WHERE statistic_id = 'sensor.aussen_temperature'
                  ) 
                  AND mean NOT IN ("unknown", "", "unavailable") 
              GROUP BY 
                  localdate 
          ) aussen_temperature ON flowmeter_sum.localdate = aussen_temperature.localdate
          LEFT JOIN (
              SELECT 
                  ROUND(max(mean), 2) AS state,
                  DATE(datetime(created_ts, 'unixepoch', 'localtime')) AS localdate 
              FROM 
                  statistics 
              WHERE 
                  metadata_id = (
                      SELECT id 
                      FROM statistics_meta 
                      WHERE statistic_id = 'sensor.flowmeter'
                  ) 
                  AND max NOT IN ("unknown", "", "unavailable") 
              GROUP BY 
                  localdate 
          ) flowmeter ON flowmeter.localdate = aussen_temperature.localdate
          LEFT JOIN (
              SELECT 
                  ROUND(min(min), 2) AS state_min,
                  DATE(datetime(created_ts, 'unixepoch', 'localtime')) AS localdate 
              FROM 
                  statistics 
              WHERE 
                  metadata_id = (
                      SELECT id 
                      FROM statistics_meta 
                      WHERE statistic_id = 'sensor.heating_ruecklauf'
                  ) 
                  AND max NOT IN ("unknown", "", "unavailable") 
              GROUP BY 
                  localdate 
          ) heating_ruecklauf ON flowmeter.localdate = heating_ruecklauf.localdate
          LEFT JOIN (
              SELECT 
                  ROUND(min(min), 2) AS state_min,
                  DATE(datetime(created_ts, 'unixepoch', 'localtime')) AS localdate 
              FROM 
                  statistics 
              WHERE 
                  metadata_id = (
                      SELECT id 
                      FROM statistics_meta 
                      WHERE statistic_id = 'sensor.heating_vorlauf'
                  ) 
                  AND max NOT IN ("unknown", "", "unavailable") 
              GROUP BY 
                  localdate 
          ) heating_vorlauf ON heating_ruecklauf.localdate = heating_vorlauf.localdate;
        value_template: '{{ value_json[0].state }}'
        column: json      

Markdown-Card-Inhalt:

[+]
{% set data = state_attr('sensor.daily_heating','json')  %}
<table><tr>
  <th>Datum</th>
  <th align=right>Flowmeter SUM</th>
  <th align=right>AVG Aussen</th>
  <th align=right>Flowmeter</th>
  <th align=right>Delta</th>
  <th align=right>Vorlauf</th>
  <th align=right>Rücklauf</th>
</tr>
{% for i in range(0,data | count)%}
 <tr>
   <td align=right>
     {{data[i].localdate  }}
   </td>
   <td align=right>
     {{ '{:.2f}'.format(data[i].flowmeter_sum | round(2))  }} m³
   </td>
   <td align=right>
     {{ '{:.2f}'.format(data[i].aussen_temperatur_mean | round(2, 'floor')) }} °C
   </td>
   <td align=right>
     {{ '{:.2f}'.format(data[i].flowmeter| round(2, 'floor'))}} m³/h
   </td>
   <td align=right >
     {{ '{:.1f}'.format((data[i].heating_vorlauf - data[i].heating_ruecklauf) | round(1, 'floor'))  }} °C
   </td>   
   <td align=right>
     {{ '{:.1f}'.format(data[i].heating_vorlauf | round(1, 'floor')) }} °C
   </td>   
   <td align=right>
     {{ '{:.1f}'.format(data[i].heating_ruecklauf | round(1, 'floor')) }} °C
   </td>
 </tr>
{% endfor %}

Markdown-Card Limit

Die Darstellung von Daten mit über 262144 Zeichen überfordert die Markdown-Card und führt zu einem Fehler:

(Template output exceeded maximum size of 262144 characters)

 Eine Möglichkeit das Limit der Markdown-Card zu umgehen ist ein Helper für die Anzeige von Seiten: "input_number.heating_pagination"

Der Helfer kann dann in der Markdown-Card verwendet werden. Die Variable "numlist" limitiert die Anzahl der Einträge pro Seite:

[+]
type: markdown
content: |
  {% set data = state_attr('sensor.daily_heating','json')  %}
  {% set pagination = states('input_number.heating_pagination')  | int(0)   %}
  {% set numlist = 500  %}
  {% set start = (pagination * numlist) - numlist %}
  {% set end = start + numlist %}
  {% if(end > (data | count)) %}
  {% set end = data | count %}
  {% endif %}

  <table><tr>
    <th>Datum</th>
    <th align=right>Flowmeter SUM</th>
    <th align=right>AVG Aussen</th>
    <th align=right>Flowmeter</th>
    <th align=right>Runtime</th>
    <th align=right>Delta</th>
    <th align=right>Heizleistung</th>
    <th align=right>Vorlauf</th>
    <th align=right>Rücklauf</th>
  </tr>{#data | count#}
  {% for i in range(start, end)%}
   <tr>
     <td align=right>
       {{data[i].localdate  }}
     </td>
     <td align=right>
       {% if (data[i].flowmeter | float(0) > 0.5) %}{{ '{:.2f}'.format(data[i].flowmeter_sum | float(0) | round(2))  }}{% else %}-{% endif %} 
     </td>
     <td align=right>
       {{data[i].aussen_temperatur_mean | float("n/a")}} °C
     </td>
     <td align=right>
       {{ '{:.2f}'.format(data[i].flowmeter| float(0) | round(2, 'floor'))}} m³/h
     </td>   
     <td align=right>
       {% if (data[i].flowmeter | float(0) > 0.5) %}{{ '{:.2f}'.format(data[i].flowmeter_sum | float(0) / data[i].flowmeter | float(0) | round(2, 'floor'))}}{% else %}-{% endif %}h
     </td>
     <td align=right >
       {% if (data[i].flowmeter | float(0) > 0.5) %}{{ '{:.1f}'.format((data[i].heating_vorlauf | float(0)  - (data[i].heating_ruecklauf) | float(0)) | round(1, 'floor'))  }}{% else %}-{% endif %} °C
     </td>     <td align=right >
       {% if (data[i].flowmeter | float(0) > 0.5) %}{{ '{:.1f}'.format(((data[i].heating_vorlauf | float(0)  - (data[i].heating_ruecklauf) | float(0))) * 1.163 * data[i].flowmeter_sum | float(0) | round(2, 'floor'))  }}{% else %}-{% endif %} kWh
     </td>    
     <td align=right>
       {{ '{:.1f}'.format(data[i].heating_vorlauf | float(0) | round(2, 'floor')) }} °C
     </td>   
     <td align=right>
       {{ '{:.1f}'.format(data[i].heating_ruecklauf | float(0) | round(2, 'floor')) }} °C
     </td>

   </tr>
  {% endfor %}
grid_options:
  columns: full
text_only: true
card_mod:
  style:
    ha-markdown:
      $:
        ha-markdown-element: |
          table {
            width: 100%;
            padding: 10px;
            margin: -20px!important;
          }
          th, td {
            padding: 4px;                      
            overflow: hidden; 
            text-overflow: ellipsis;
            white-space: nowrap;
          }
          tr:nth-child(even) {
            background-color: var(--secondary-background-color);
          }

Das Beispiel zeigt die "Code-Editor-Ansicht (YAML)" und nutzt die HACS-Integration "Card_Mod" und damit umgesetzt CSS-Styles:

Durch Hinzufügen des Helfers für die aktuelle Seite lässt sich in 500er Schritten zwischen den Seiten wechseln.

alternative Anzeige: HACS-Integration: Flex Table

Etwas komfortabler beim Einrichten, dafür aber weniger flexibel ist die Integration Flex Table:

Für einfache Tabellen ok, aber spätestens beim Verknüpfen oder Berechnen bestimmter Spalten stößt die Flex Table-Card an ihre Grenzen, daher würde ich die Markdown-Card der Flex-table Card vorziehen.

ⓘ Daten direkt aufrufen, Plotly-Graph-Table

Die Tabellenansicht von Plotly-Graph darf an dieser Stelle nicht unerwähnt bleiben. Eigentlich für die Anzeige von Charts kann Plotly-Graph historische Daten auch als Tabelle anzeigen: Direkt und ohne vorab eine Query zu verwenden.

[+]
type: custom:plotly-graph
hours_to_show: 999
entities:
  - entity: sensor.pv_panels_energy
    type: table
    period:
      "0": day
    statistic: state
    columnwidth:
      - 16
      - 20
    header:
      values:
        - Date
        - value
      fill:
        color: $ex css_vars["primary-color"]
      font:
        color: $ex css_vars["primary-text-color"]
    filters:
      - delta
    cells:
      values:
        - |
          $ex xs.toReversed().map(x=>
            new Intl.DateTimeFormat('de-DE', {
              day: '2-digit',
              month: '2-digit',
              year: '2-digit'
            }).format(x))
        - $ex ys.toReversed()
      align:
        - center
        - left
      fill:
        color: $ex css_vars["card-background-color"]

Beispiel 2: mehrere Spalten mit verschiedenen Sensoren:

[+]
type: custom:plotly-graph
hours_to_show: 999999
entities:
  - entity: sensor.heating_water_energy
    type: table
    period:
      "0": day
    statistic: sum
    filters:
      - delta
      - store_var: water
  - entity: sensor.flowmeter_sum
    type: table
    period:
      "0": day
    statistic: sum
    columnwidth:
      - 16
      - 10
      - 10
    header:
      values:
        - Date
        - Flowmeter
        - Water
      fill:
        color: $ex css_vars["primary-color"]
      font:
        color: $ex css_vars["primary-text-color"]
    filters:
      - delta
    cells:
      values:
        - |
          $ex xs.toReversed().map(x=>
            new Intl.DateTimeFormat('de-DE', {
              day: '2-digit',
              month: '2-digit',
              year: '2-digit'
            }).format(x))
        - $ex ys.toReversed().map(x=> x.toFixed(2))
        - $ex vars.water.ys.map(x=> x.toFixed(2))
      align:
        - center
        - left
      fill:
        color: $ex css_vars["card-background-color"]
grid_options:
  columns: full
  rows: 12

Die Tabellenanischt in Plotly: funktioniert, ist aber definitiv nicht dessen Kernkompetenz. Plotly überzeugt hier weder optisch noch in der Bedienung (Scrollverhalten). Die Daten können auch nicht brauchbar exportiert, oder in die Zwischenablage kopiert werden. Aus den genannten Gründen kann ich Plotly für die Anzeige von Tabelle nicht empfehlen.

Fazit

Home Assistant hat seine Stärken beim Anzeigen aktueller Werte und besitzt großartige Möglichkeiten zur Datenvisualisierung. Dennoch bieten bestimmte andere Lösungen hier mehr für das Speichern und Anzeigen historischer Daten. Nicht nur beim Visualisieren von Charts, auch bei der Anzeige von Tabellen könnte sich Home Assistant ein Beispiel an anderen Visualisierungslösungen, wie Grafana nehmen.

positive Bewertung({{pro_count}})
Beitrag bewerten:
{{percentage}} % positiv
negative Bewertung({{con_count}})

DANKE für deine Bewertung!

Fragen / Kommentare


 
Durch die weitere Nutzung der Seite stimmst du der Verwendung von Cookies zu Mehr Details