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:
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 %} m³
</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.

{{percentage}} % positiv
