Home Assistant Daten als Tabelle anzeigen / Reporting

Charts sind super, aber für bestimmte Auswertungen wäre eine einfache Tabellenansicht historischer Daten wünschenswert. Bedauerlicherweise hat Home Assistant in Sachen Reporting, out of the box, sehr wenig zu bieten. Nur bestimmte Cards können auf die History Daten zugreifen, dann aber ohne einer Möglichkeit die Daten aufzubereiten, zu filtern, oder anzupassen.

Über Umwege ist es möglich beliebige SQL-Abfragen der Datenbank in einer Lovelace-Card als Tabelle auszugeben.  Warum das so ist, siehe: Home Assistant Besonderheiten: Stärken und Schwächen

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 Lovelace-Card angezeigt werden:

☑ SQL-Integration: JSON

Mithilfe der vorhandenen SQL-Integration ist es möglich, das Ergebnis für bestimmte SQL-Abfragen in einem eigenen Sensor zu speichern, siehe: Home Assistant SQL - Integration. Gespeichert im JSON-Format, können die Daten relativ einfach ü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):

[+]
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
          );

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 speichert die Werte als Attributes, wodurch es, im Gegensatz zum Entity-Wert mehr als 255 Zeichen zu speichern:

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

Markdown:

[+]
{% set data = state_attr('sensor.daily_pv_yield','json') | from_json  %}
<table><tr>
  <th>Datum</th>
  <th>Ertrag</th>
</tr>
{% for i in range(0,data | count)%}
 <tr>
   <td align=center>
     {{data[i].localdate  }}
   </td>
   <td align=center>
     {{ '{:.2f}'.format(data[i].state | round(2) ) | replace(".",",")  }} kWh
   </td>
 </tr>
{% endfor %}

Achtung bei großen Datenmengen: Abfragen laufen alle 30 Sekunden

Per Default werden die SQL-Abfragen alle 30 Sekunden ausgeführt wodurch Home Assistant sehr schnell ausgebremst werden könnte, entsprechend sollten große Abfragen aus dem Standard-Polling ausgenommen werden:

Die Query kann bei Bedarf über ein Skript oder regelmäßig über eine Automatisierung ausgeführt werden:

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:

[+]
          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;

Markdown-Card-Inhalt:

[+]
{% set data = state_attr('sensor.daily_heating','json')  | from_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')  | from_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 500-er Schritten zwischen den Seiten wechseln.

Anstelle einer fixen Anzahl, könnte auch das Jahr für die Pagination verwendet werden:

...
{% for i in range(0, (data | count)) if (strptime(data[i].localdate, '%Y-%m-%d').year == pagination)%}
...

Card-Mod Style

Beim Einsatz der HACS-Integration Card-Mod kann das Layout der Tabelle angepasst werden, wie in den Beispielen gezeigt, unterschiedliche Hintergründe für gerade und ungerade Zeilen.

[+]
...
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);
          }

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.

YouTube Video

Hier der Beitrag nochmal als YouTube Video:

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